@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
|
@@ -3,8 +3,9 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
7
|
-
const
|
|
6
|
+
const require_dist = require("../../dist-CUNGH0fs.js");
|
|
7
|
+
const require_ms$1 = require("../../ms-t68_FaqE.js");
|
|
8
|
+
const require_jwt_signing_key = require("../../jwt-signing-key-WI6z7JBo.js");
|
|
8
9
|
let node_crypto = require("node:crypto");
|
|
9
10
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
10
11
|
let crypto$1 = require("crypto");
|
|
@@ -14,7 +15,7 @@ node_path = require_chunk.__toESM(node_path);
|
|
|
14
15
|
let node_fs = require("node:fs");
|
|
15
16
|
node_fs = require_chunk.__toESM(node_fs);
|
|
16
17
|
//#region src/auth/parse-record.ts
|
|
17
|
-
function parseRecord(kind, schema, data) {
|
|
18
|
+
function parseRecord$1(kind, schema, data) {
|
|
18
19
|
try {
|
|
19
20
|
return schema.parse(data);
|
|
20
21
|
} catch (err) {
|
|
@@ -26,64 +27,26 @@ function parseRecord(kind, schema, data) {
|
|
|
26
27
|
}
|
|
27
28
|
//#endregion
|
|
28
29
|
//#region src/auth/api-key-manager.ts
|
|
30
|
+
/**
|
|
31
|
+
* Legacy API keys — LIST and REVOKE only.
|
|
32
|
+
*
|
|
33
|
+
* No boundary accepts one (the tRPC context has no branch for them) and
|
|
34
|
+
* nothing creates them any more: what is left is the rows a hub may still
|
|
35
|
+
* hold, so an operator can see and delete them. Scoped tokens (`cst_`) are
|
|
36
|
+
* the credential for scripts and integrations.
|
|
37
|
+
*/
|
|
29
38
|
var API_KEYS_COLLECTION$1 = "api_keys";
|
|
30
39
|
function parseApiKey(data) {
|
|
31
|
-
return parseRecord("api-key", require_dist.ApiKeyRecordSchema, data);
|
|
40
|
+
return parseRecord$1("api-key", require_dist.ApiKeyRecordSchema, data);
|
|
32
41
|
}
|
|
33
42
|
var ApiKeyManager = class {
|
|
34
43
|
storageAccess;
|
|
35
|
-
|
|
36
|
-
constructor(storageAccess, auth) {
|
|
44
|
+
constructor(storageAccess) {
|
|
37
45
|
this.storageAccess = storageAccess;
|
|
38
|
-
this.auth = auth;
|
|
39
46
|
}
|
|
40
47
|
get store() {
|
|
41
48
|
return this.storageAccess.getStore();
|
|
42
49
|
}
|
|
43
|
-
async create(input) {
|
|
44
|
-
const { token: rawToken, hash, prefix } = this.auth.generateApiKey();
|
|
45
|
-
const now = Date.now();
|
|
46
|
-
const record = {
|
|
47
|
-
id: node_crypto.randomUUID(),
|
|
48
|
-
label: input.label,
|
|
49
|
-
isAdmin: input.isAdmin ?? false,
|
|
50
|
-
allowedProviders: input.allowedProviders ?? "*",
|
|
51
|
-
allowedDevices: input.allowedDevices ?? {},
|
|
52
|
-
tokenHash: hash,
|
|
53
|
-
tokenPrefix: prefix,
|
|
54
|
-
createdAt: now
|
|
55
|
-
};
|
|
56
|
-
await this.store.insert.mutate({
|
|
57
|
-
collection: API_KEYS_COLLECTION$1,
|
|
58
|
-
record: {
|
|
59
|
-
id: record.id,
|
|
60
|
-
data: { ...record }
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
return {
|
|
64
|
-
record,
|
|
65
|
-
token: rawToken
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
async validateToken(token) {
|
|
69
|
-
const allKeys = await this.store.query.query({ collection: API_KEYS_COLLECTION$1 });
|
|
70
|
-
for (const entry of allKeys) {
|
|
71
|
-
const record = parseApiKey(entry.data);
|
|
72
|
-
if (this.auth.validateApiKey(token, record.tokenHash)) {
|
|
73
|
-
const updatedData = {
|
|
74
|
-
...record,
|
|
75
|
-
lastUsedAt: Date.now()
|
|
76
|
-
};
|
|
77
|
-
await this.store.update.mutate({
|
|
78
|
-
collection: API_KEYS_COLLECTION$1,
|
|
79
|
-
id: record.id,
|
|
80
|
-
data: { ...updatedData }
|
|
81
|
-
});
|
|
82
|
-
return updatedData;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
50
|
async listAll() {
|
|
88
51
|
return (await this.store.query.query({ collection: API_KEYS_COLLECTION$1 })).map((r) => {
|
|
89
52
|
const { tokenHash: _th, ...rest } = parseApiKey(r.data);
|
|
@@ -196,7 +159,7 @@ function genSalt(rounds, seed_length, callback) {
|
|
|
196
159
|
* @returns {!Promise} If `callback` has been omitted
|
|
197
160
|
* @throws {Error} If `callback` is present but not a function
|
|
198
161
|
*/
|
|
199
|
-
function hash(password, salt, callback, progressCallback) {
|
|
162
|
+
function hash$1(password, salt, callback, progressCallback) {
|
|
200
163
|
function _async(callback) {
|
|
201
164
|
if (typeof password === "string" && typeof salt === "number") genSalt(salt, function(err, salt) {
|
|
202
165
|
_hash(password, salt, callback, progressCallback);
|
|
@@ -249,7 +212,7 @@ function compare(password, hashValue, callback, progressCallback) {
|
|
|
249
212
|
nextTick(callback.bind(this, null, false));
|
|
250
213
|
return;
|
|
251
214
|
}
|
|
252
|
-
hash(password, hashValue.substring(0, 29), function(err, comp) {
|
|
215
|
+
hash$1(password, hashValue.substring(0, 29), function(err, comp) {
|
|
253
216
|
if (err) callback(err);
|
|
254
217
|
else callback(null, safeStringCompare(comp, hashValue));
|
|
255
218
|
}, progressCallback);
|
|
@@ -1897,49 +1860,9 @@ function _hash(password, salt, callback, progressCallback) {
|
|
|
1897
1860
|
}, progressCallback);
|
|
1898
1861
|
}
|
|
1899
1862
|
//#endregion
|
|
1900
|
-
//#region ../../node_modules/safe-buffer/index.js
|
|
1901
|
-
var require_safe_buffer = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
|
|
1902
|
-
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
1903
|
-
var buffer = require("buffer");
|
|
1904
|
-
var Buffer = buffer.Buffer;
|
|
1905
|
-
function copyProps(src, dst) {
|
|
1906
|
-
for (var key in src) dst[key] = src[key];
|
|
1907
|
-
}
|
|
1908
|
-
if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) module.exports = buffer;
|
|
1909
|
-
else {
|
|
1910
|
-
copyProps(buffer, exports);
|
|
1911
|
-
exports.Buffer = SafeBuffer;
|
|
1912
|
-
}
|
|
1913
|
-
function SafeBuffer(arg, encodingOrOffset, length) {
|
|
1914
|
-
return Buffer(arg, encodingOrOffset, length);
|
|
1915
|
-
}
|
|
1916
|
-
SafeBuffer.prototype = Object.create(Buffer.prototype);
|
|
1917
|
-
copyProps(Buffer, SafeBuffer);
|
|
1918
|
-
SafeBuffer.from = function(arg, encodingOrOffset, length) {
|
|
1919
|
-
if (typeof arg === "number") throw new TypeError("Argument must not be a number");
|
|
1920
|
-
return Buffer(arg, encodingOrOffset, length);
|
|
1921
|
-
};
|
|
1922
|
-
SafeBuffer.alloc = function(size, fill, encoding) {
|
|
1923
|
-
if (typeof size !== "number") throw new TypeError("Argument must be a number");
|
|
1924
|
-
var buf = Buffer(size);
|
|
1925
|
-
if (fill !== void 0) if (typeof encoding === "string") buf.fill(fill, encoding);
|
|
1926
|
-
else buf.fill(fill);
|
|
1927
|
-
else buf.fill(0);
|
|
1928
|
-
return buf;
|
|
1929
|
-
};
|
|
1930
|
-
SafeBuffer.allocUnsafe = function(size) {
|
|
1931
|
-
if (typeof size !== "number") throw new TypeError("Argument must be a number");
|
|
1932
|
-
return Buffer(size);
|
|
1933
|
-
};
|
|
1934
|
-
SafeBuffer.allocUnsafeSlow = function(size) {
|
|
1935
|
-
if (typeof size !== "number") throw new TypeError("Argument must be a number");
|
|
1936
|
-
return buffer.SlowBuffer(size);
|
|
1937
|
-
};
|
|
1938
|
-
}));
|
|
1939
|
-
//#endregion
|
|
1940
1863
|
//#region ../../node_modules/jws/lib/data-stream.js
|
|
1941
1864
|
var require_data_stream = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
|
|
1942
|
-
var Buffer = require_safe_buffer().Buffer;
|
|
1865
|
+
var Buffer = require_ms$1.require_safe_buffer().Buffer;
|
|
1943
1866
|
var Stream$2 = require("stream");
|
|
1944
1867
|
var util$3 = require("util");
|
|
1945
1868
|
function DataStream(data) {
|
|
@@ -2002,7 +1925,7 @@ var require_param_bytes_for_alg = /* @__PURE__ */ require_chunk.__commonJSMin(((
|
|
|
2002
1925
|
//#endregion
|
|
2003
1926
|
//#region ../../node_modules/ecdsa-sig-formatter/src/ecdsa-sig-formatter.js
|
|
2004
1927
|
var require_ecdsa_sig_formatter = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
|
|
2005
|
-
var Buffer = require_safe_buffer().Buffer;
|
|
1928
|
+
var Buffer = require_ms$1.require_safe_buffer().Buffer;
|
|
2006
1929
|
var getParamBytesForAlg = require_param_bytes_for_alg();
|
|
2007
1930
|
var MAX_OCTET = 128, CLASS_UNIVERSAL = 0, PRIMITIVE_BIT = 32, TAG_SEQ = 16, TAG_INT = 2, ENCODED_TAG_SEQ = TAG_SEQ | PRIMITIVE_BIT | CLASS_UNIVERSAL << 6, ENCODED_TAG_INT = TAG_INT | CLASS_UNIVERSAL << 6;
|
|
2008
1931
|
function base64Url(base64) {
|
|
@@ -2119,7 +2042,7 @@ var require_buffer_equal_constant_time = /* @__PURE__ */ require_chunk.__commonJ
|
|
|
2119
2042
|
//#endregion
|
|
2120
2043
|
//#region ../../node_modules/jwa/index.js
|
|
2121
2044
|
var require_jwa = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
|
|
2122
|
-
var Buffer = require_safe_buffer().Buffer;
|
|
2045
|
+
var Buffer = require_ms$1.require_safe_buffer().Buffer;
|
|
2123
2046
|
var crypto$2 = require("crypto");
|
|
2124
2047
|
var formatEcdsa = require_ecdsa_sig_formatter();
|
|
2125
2048
|
var util$2 = require("util");
|
|
@@ -2305,7 +2228,7 @@ var require_tostring = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, mo
|
|
|
2305
2228
|
//#endregion
|
|
2306
2229
|
//#region ../../node_modules/jws/lib/sign-stream.js
|
|
2307
2230
|
var require_sign_stream = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
|
|
2308
|
-
var Buffer = require_safe_buffer().Buffer;
|
|
2231
|
+
var Buffer = require_ms$1.require_safe_buffer().Buffer;
|
|
2309
2232
|
var DataStream = require_data_stream();
|
|
2310
2233
|
var jwa = require_jwa();
|
|
2311
2234
|
var Stream$1 = require("stream");
|
|
@@ -2374,7 +2297,7 @@ var require_sign_stream = /* @__PURE__ */ require_chunk.__commonJSMin(((exports,
|
|
|
2374
2297
|
//#endregion
|
|
2375
2298
|
//#region ../../node_modules/jws/lib/verify-stream.js
|
|
2376
2299
|
var require_verify_stream = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
|
|
2377
|
-
var Buffer = require_safe_buffer().Buffer;
|
|
2300
|
+
var Buffer = require_ms$1.require_safe_buffer().Buffer;
|
|
2378
2301
|
var DataStream = require_data_stream();
|
|
2379
2302
|
var jwa = require_jwa();
|
|
2380
2303
|
var Stream = require("stream");
|
|
@@ -2567,127 +2490,9 @@ var require_TokenExpiredError = /* @__PURE__ */ require_chunk.__commonJSMin(((ex
|
|
|
2567
2490
|
module.exports = TokenExpiredError;
|
|
2568
2491
|
}));
|
|
2569
2492
|
//#endregion
|
|
2570
|
-
//#region ../../node_modules/ms/index.js
|
|
2571
|
-
var require_ms = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
|
|
2572
|
-
/**
|
|
2573
|
-
* Helpers.
|
|
2574
|
-
*/
|
|
2575
|
-
var s = 1e3;
|
|
2576
|
-
var m = s * 60;
|
|
2577
|
-
var h = m * 60;
|
|
2578
|
-
var d = h * 24;
|
|
2579
|
-
var w = d * 7;
|
|
2580
|
-
var y = d * 365.25;
|
|
2581
|
-
/**
|
|
2582
|
-
* Parse or format the given `val`.
|
|
2583
|
-
*
|
|
2584
|
-
* Options:
|
|
2585
|
-
*
|
|
2586
|
-
* - `long` verbose formatting [false]
|
|
2587
|
-
*
|
|
2588
|
-
* @param {String|Number} val
|
|
2589
|
-
* @param {Object} [options]
|
|
2590
|
-
* @throws {Error} throw an error if val is not a non-empty string or a number
|
|
2591
|
-
* @return {String|Number}
|
|
2592
|
-
* @api public
|
|
2593
|
-
*/
|
|
2594
|
-
module.exports = function(val, options) {
|
|
2595
|
-
options = options || {};
|
|
2596
|
-
var type = typeof val;
|
|
2597
|
-
if (type === "string" && val.length > 0) return parse(val);
|
|
2598
|
-
else if (type === "number" && isFinite(val)) return options.long ? fmtLong(val) : fmtShort(val);
|
|
2599
|
-
throw new Error("val is not a non-empty string or a valid number. val=" + JSON.stringify(val));
|
|
2600
|
-
};
|
|
2601
|
-
/**
|
|
2602
|
-
* Parse the given `str` and return milliseconds.
|
|
2603
|
-
*
|
|
2604
|
-
* @param {String} str
|
|
2605
|
-
* @return {Number}
|
|
2606
|
-
* @api private
|
|
2607
|
-
*/
|
|
2608
|
-
function parse(str) {
|
|
2609
|
-
str = String(str);
|
|
2610
|
-
if (str.length > 100) return;
|
|
2611
|
-
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);
|
|
2612
|
-
if (!match) return;
|
|
2613
|
-
var n = parseFloat(match[1]);
|
|
2614
|
-
switch ((match[2] || "ms").toLowerCase()) {
|
|
2615
|
-
case "years":
|
|
2616
|
-
case "year":
|
|
2617
|
-
case "yrs":
|
|
2618
|
-
case "yr":
|
|
2619
|
-
case "y": return n * y;
|
|
2620
|
-
case "weeks":
|
|
2621
|
-
case "week":
|
|
2622
|
-
case "w": return n * w;
|
|
2623
|
-
case "days":
|
|
2624
|
-
case "day":
|
|
2625
|
-
case "d": return n * d;
|
|
2626
|
-
case "hours":
|
|
2627
|
-
case "hour":
|
|
2628
|
-
case "hrs":
|
|
2629
|
-
case "hr":
|
|
2630
|
-
case "h": return n * h;
|
|
2631
|
-
case "minutes":
|
|
2632
|
-
case "minute":
|
|
2633
|
-
case "mins":
|
|
2634
|
-
case "min":
|
|
2635
|
-
case "m": return n * m;
|
|
2636
|
-
case "seconds":
|
|
2637
|
-
case "second":
|
|
2638
|
-
case "secs":
|
|
2639
|
-
case "sec":
|
|
2640
|
-
case "s": return n * s;
|
|
2641
|
-
case "milliseconds":
|
|
2642
|
-
case "millisecond":
|
|
2643
|
-
case "msecs":
|
|
2644
|
-
case "msec":
|
|
2645
|
-
case "ms": return n;
|
|
2646
|
-
default: return;
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
/**
|
|
2650
|
-
* Short format for `ms`.
|
|
2651
|
-
*
|
|
2652
|
-
* @param {Number} ms
|
|
2653
|
-
* @return {String}
|
|
2654
|
-
* @api private
|
|
2655
|
-
*/
|
|
2656
|
-
function fmtShort(ms) {
|
|
2657
|
-
var msAbs = Math.abs(ms);
|
|
2658
|
-
if (msAbs >= d) return Math.round(ms / d) + "d";
|
|
2659
|
-
if (msAbs >= h) return Math.round(ms / h) + "h";
|
|
2660
|
-
if (msAbs >= m) return Math.round(ms / m) + "m";
|
|
2661
|
-
if (msAbs >= s) return Math.round(ms / s) + "s";
|
|
2662
|
-
return ms + "ms";
|
|
2663
|
-
}
|
|
2664
|
-
/**
|
|
2665
|
-
* Long format for `ms`.
|
|
2666
|
-
*
|
|
2667
|
-
* @param {Number} ms
|
|
2668
|
-
* @return {String}
|
|
2669
|
-
* @api private
|
|
2670
|
-
*/
|
|
2671
|
-
function fmtLong(ms) {
|
|
2672
|
-
var msAbs = Math.abs(ms);
|
|
2673
|
-
if (msAbs >= d) return plural(ms, msAbs, d, "day");
|
|
2674
|
-
if (msAbs >= h) return plural(ms, msAbs, h, "hour");
|
|
2675
|
-
if (msAbs >= m) return plural(ms, msAbs, m, "minute");
|
|
2676
|
-
if (msAbs >= s) return plural(ms, msAbs, s, "second");
|
|
2677
|
-
return ms + " ms";
|
|
2678
|
-
}
|
|
2679
|
-
/**
|
|
2680
|
-
* Pluralization helper.
|
|
2681
|
-
*/
|
|
2682
|
-
function plural(ms, msAbs, n, name) {
|
|
2683
|
-
var isPlural = msAbs >= n * 1.5;
|
|
2684
|
-
return Math.round(ms / n) + " " + name + (isPlural ? "s" : "");
|
|
2685
|
-
}
|
|
2686
|
-
}));
|
|
2687
|
-
//#endregion
|
|
2688
2493
|
//#region ../../node_modules/jsonwebtoken/lib/timespan.js
|
|
2689
2494
|
var require_timespan = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
|
|
2690
|
-
var ms = require_ms();
|
|
2495
|
+
var ms = require_ms$1.require_ms();
|
|
2691
2496
|
module.exports = function(time, iat) {
|
|
2692
2497
|
var timestamp = iat || Math.floor(Date.now() / 1e3);
|
|
2693
2498
|
if (typeof time === "string") {
|
|
@@ -6231,7 +6036,15 @@ var AuthManager = class {
|
|
|
6231
6036
|
get jwtSecret() {
|
|
6232
6037
|
return this.signingKey.current();
|
|
6233
6038
|
}
|
|
6234
|
-
|
|
6039
|
+
/**
|
|
6040
|
+
* @param options.expiresInSec an explicit lifetime (a session's access
|
|
6041
|
+
* token): wins over `auth.tokenExpiry`, bounded to at least one minute.
|
|
6042
|
+
*/
|
|
6043
|
+
signToken(payload, options = {}) {
|
|
6044
|
+
if (options.expiresInSec !== void 0) {
|
|
6045
|
+
const seconds = Math.max(60, Math.floor(options.expiresInSec));
|
|
6046
|
+
return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn: seconds });
|
|
6047
|
+
}
|
|
6235
6048
|
const configured = this.readConfiguredExpiry();
|
|
6236
6049
|
const parsed = parseTokenExpiry(configured);
|
|
6237
6050
|
if (!parsed.ok) {
|
|
@@ -6273,42 +6086,14 @@ var AuthManager = class {
|
|
|
6273
6086
|
return import_jsonwebtoken.verify(token, this.jwtSecret);
|
|
6274
6087
|
}
|
|
6275
6088
|
async hashPassword(password) {
|
|
6276
|
-
return hash(password, 10);
|
|
6089
|
+
return hash$1(password, 10);
|
|
6277
6090
|
}
|
|
6278
6091
|
async comparePassword(password, hash) {
|
|
6279
6092
|
return compare(password, hash);
|
|
6280
6093
|
}
|
|
6281
|
-
generateApiKey() {
|
|
6282
|
-
const token = node_crypto.randomBytes(32).toString("hex");
|
|
6283
|
-
return {
|
|
6284
|
-
token,
|
|
6285
|
-
hash: node_crypto.createHash("sha256").update(token).digest("hex"),
|
|
6286
|
-
prefix: token.slice(0, 8)
|
|
6287
|
-
};
|
|
6288
|
-
}
|
|
6289
|
-
validateApiKey(token, storedHash) {
|
|
6290
|
-
return node_crypto.createHash("sha256").update(token).digest("hex") === storedHash;
|
|
6291
|
-
}
|
|
6292
|
-
/**
|
|
6293
|
-
* Create a service token for agent/worker authentication.
|
|
6294
|
-
* Used when forking workers or when agents register.
|
|
6295
|
-
*/
|
|
6296
|
-
createServiceToken(opts) {
|
|
6297
|
-
const payload = {
|
|
6298
|
-
userId: opts.agentId,
|
|
6299
|
-
username: opts.agentId,
|
|
6300
|
-
isAdmin: true,
|
|
6301
|
-
type: "service",
|
|
6302
|
-
agentId: opts.agentId,
|
|
6303
|
-
allowedProviders: "*",
|
|
6304
|
-
allowedDevices: {}
|
|
6305
|
-
};
|
|
6306
|
-
const expiresIn = opts.expiresIn ?? "24h";
|
|
6307
|
-
return import_jsonwebtoken.sign(payload, this.jwtSecret, { expiresIn });
|
|
6308
|
-
}
|
|
6309
6094
|
/**
|
|
6310
6095
|
* Mint a short-lived HMAC-signed bridge token used by SSO-style auth
|
|
6311
|
-
* providers (OIDC, SAML,
|
|
6096
|
+
* providers (OIDC, SAML, …) to hand the post-callback
|
|
6312
6097
|
* claims to `/api/auth/sso/finish` without trusting unsigned query
|
|
6313
6098
|
* parameters. The endpoint verifies the token signature with the same
|
|
6314
6099
|
* `jwtSecret` and only then mints the user session JWT. This closes
|
|
@@ -6338,7 +6123,8 @@ var AuthManager = class {
|
|
|
6338
6123
|
...payload.sessionId !== void 0 ? { sessionId: payload.sessionId } : {},
|
|
6339
6124
|
...payload.refreshTtl !== void 0 ? { refreshTtl: payload.refreshTtl } : {},
|
|
6340
6125
|
...payload.linkIntent !== void 0 ? { linkIntent: payload.linkIntent } : {},
|
|
6341
|
-
...payload.issuer !== void 0 ? { issuer: payload.issuer } : {}
|
|
6126
|
+
...payload.issuer !== void 0 ? { issuer: payload.issuer } : {},
|
|
6127
|
+
...payload.browserBinding !== void 0 ? { browserBinding: payload.browserBinding } : {}
|
|
6342
6128
|
};
|
|
6343
6129
|
/**
|
|
6344
6130
|
* `'never'` mints with NO `exp` claim, and the option must be OMITTED to do
|
|
@@ -6382,6 +6168,7 @@ var AuthManager = class {
|
|
|
6382
6168
|
const sessionId = typeof decoded["sessionId"] === "string" ? decoded["sessionId"] : void 0;
|
|
6383
6169
|
const linkIntent = typeof decoded["linkIntent"] === "string" ? decoded["linkIntent"] : void 0;
|
|
6384
6170
|
const issuer = typeof decoded["issuer"] === "string" ? decoded["issuer"] : void 0;
|
|
6171
|
+
const browserBinding = typeof decoded["browserBinding"] === "string" ? decoded["browserBinding"] : void 0;
|
|
6385
6172
|
const rawRefreshTtl = decoded["refreshTtl"];
|
|
6386
6173
|
const refreshTtl = rawRefreshTtl === "never" || typeof rawRefreshTtl === "number" && rawRefreshTtl > 0 ? rawRefreshTtl : void 0;
|
|
6387
6174
|
return {
|
|
@@ -6399,7 +6186,8 @@ var AuthManager = class {
|
|
|
6399
6186
|
...sessionId !== void 0 ? { sessionId } : {},
|
|
6400
6187
|
...refreshTtl !== void 0 ? { refreshTtl } : {},
|
|
6401
6188
|
...linkIntent !== void 0 ? { linkIntent } : {},
|
|
6402
|
-
...issuer !== void 0 ? { issuer } : {}
|
|
6189
|
+
...issuer !== void 0 ? { issuer } : {},
|
|
6190
|
+
...browserBinding !== void 0 ? { browserBinding } : {}
|
|
6403
6191
|
};
|
|
6404
6192
|
} catch {
|
|
6405
6193
|
return null;
|
|
@@ -6425,10 +6213,43 @@ var AuthManager = class {
|
|
|
6425
6213
|
kind: "totp-challenge",
|
|
6426
6214
|
userId: payload.userId,
|
|
6427
6215
|
username: payload.username,
|
|
6428
|
-
isAdmin: payload.isAdmin
|
|
6216
|
+
isAdmin: payload.isAdmin,
|
|
6217
|
+
...payload.method !== void 0 ? { method: payload.method } : {}
|
|
6429
6218
|
}, this.jwtSecret, { expiresIn: ttlSec });
|
|
6430
6219
|
}
|
|
6431
6220
|
/**
|
|
6221
|
+
* A short-lived proof that the signed-in user just re-entered their password
|
|
6222
|
+
* or used a passkey. Sensitive self-service actions (linking a sign-in
|
|
6223
|
+
* account, disabling 2FA, removing a passkey, minting an admin token)
|
|
6224
|
+
* require one. `kind: 'reauth'` keeps it from ever being a bearer.
|
|
6225
|
+
*/
|
|
6226
|
+
signReauthToken(claims, ttlSec = 300) {
|
|
6227
|
+
return import_jsonwebtoken.sign({
|
|
6228
|
+
kind: "reauth",
|
|
6229
|
+
userId: claims.userId,
|
|
6230
|
+
method: claims.method,
|
|
6231
|
+
...claims.sessionId !== void 0 ? { sid: claims.sessionId } : {}
|
|
6232
|
+
}, this.jwtSecret, { expiresIn: ttlSec });
|
|
6233
|
+
}
|
|
6234
|
+
/** Decode a re-authentication proof, or `null` (forged, expired, another kind). */
|
|
6235
|
+
verifyReauthToken(token) {
|
|
6236
|
+
try {
|
|
6237
|
+
const decoded = import_jsonwebtoken.verify(token, this.jwtSecret);
|
|
6238
|
+
if (decoded["kind"] !== "reauth") return null;
|
|
6239
|
+
const userId = decoded["userId"];
|
|
6240
|
+
const method = decoded["method"];
|
|
6241
|
+
const sid = decoded["sid"];
|
|
6242
|
+
if (typeof userId !== "string" || method !== "password" && method !== "passkey") return null;
|
|
6243
|
+
return {
|
|
6244
|
+
userId,
|
|
6245
|
+
method,
|
|
6246
|
+
...typeof sid === "string" ? { sessionId: sid } : {}
|
|
6247
|
+
};
|
|
6248
|
+
} catch {
|
|
6249
|
+
return null;
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
/**
|
|
6432
6253
|
* Verify + decode a TOTP challenge token. Returns `null` for any
|
|
6433
6254
|
* failure (invalid signature, expired, wrong `kind`, missing
|
|
6434
6255
|
* claims). The caller MUST also verify the 6-digit code against
|
|
@@ -6443,10 +6264,12 @@ var AuthManager = class {
|
|
|
6443
6264
|
const username = decoded["username"];
|
|
6444
6265
|
const isAdmin = decoded["isAdmin"];
|
|
6445
6266
|
if (typeof userId !== "string" || typeof username !== "string" || typeof isAdmin !== "boolean") return null;
|
|
6267
|
+
const method = decoded["method"];
|
|
6446
6268
|
return {
|
|
6447
6269
|
userId,
|
|
6448
6270
|
username,
|
|
6449
|
-
isAdmin
|
|
6271
|
+
isAdmin,
|
|
6272
|
+
...method === "password" || method === "passkey" || method === "sso" ? { method } : {}
|
|
6450
6273
|
};
|
|
6451
6274
|
} catch {
|
|
6452
6275
|
return null;
|
|
@@ -6458,7 +6281,7 @@ var AuthManager = class {
|
|
|
6458
6281
|
var TOKENS_COLLECTION = "scoped_tokens";
|
|
6459
6282
|
var TOKEN_PREFIX = "cst_";
|
|
6460
6283
|
function parseToken(data) {
|
|
6461
|
-
return parseRecord("scoped-token", require_dist.ScopedTokenSchema, data);
|
|
6284
|
+
return parseRecord$1("scoped-token", require_dist.ScopedTokenSchema, data);
|
|
6462
6285
|
}
|
|
6463
6286
|
/**
|
|
6464
6287
|
* Manages scoped API tokens with restricted addon/route/capability access.
|
|
@@ -6620,6 +6443,808 @@ var ScopedTokenManager = class {
|
|
|
6620
6443
|
}
|
|
6621
6444
|
};
|
|
6622
6445
|
//#endregion
|
|
6446
|
+
//#region src/auth/login-session-manager.ts
|
|
6447
|
+
/**
|
|
6448
|
+
* Login sessions — the server-side record behind every session token.
|
|
6449
|
+
*
|
|
6450
|
+
* A session token used to be a self-contained 30-day JWT: nothing on the hub
|
|
6451
|
+
* knew it existed, so logout revoked nothing, a password change left every
|
|
6452
|
+
* other device signed in, and a deleted user's sessions lived until they
|
|
6453
|
+
* expired. Now every login creates a row here and the token carries its id
|
|
6454
|
+
* (`sid`); the hub checks the row on every request (`check`, an in-memory
|
|
6455
|
+
* lookup), so revoking a row ends the session at once.
|
|
6456
|
+
*
|
|
6457
|
+
* Two kinds of client:
|
|
6458
|
+
* - **refreshable** — gets a short access token (default 2 h) and an opaque
|
|
6459
|
+
* refresh token (`csr_…`, stored hashed). `refresh` rotates the refresh
|
|
6460
|
+
* token and slides the session's expiry; presenting a rotated-out refresh
|
|
6461
|
+
* token after a short grace window is treated as theft and revokes the
|
|
6462
|
+
* whole session.
|
|
6463
|
+
* - **legacy** — a client from before refresh tokens: its access token lives
|
|
6464
|
+
* as long as the session, and it renews through the handoff-code pair
|
|
6465
|
+
* (`extend`). Kept for one release so installed viewer apps keep working.
|
|
6466
|
+
*
|
|
6467
|
+
* `legacyCutoff(userId)` covers tokens minted before sessions existed (no
|
|
6468
|
+
* `sid`): a token of that user issued before the cutoff is refused. Revoking
|
|
6469
|
+
* all of a user's sessions moves the cutoff, so a password change or a user
|
|
6470
|
+
* deletion also ends their pre-session tokens.
|
|
6471
|
+
*
|
|
6472
|
+
* Mirror: every row is held in memory (a homelab hub has tens of sessions) and
|
|
6473
|
+
* written through to the store; revocations update the mirror FIRST, so a
|
|
6474
|
+
* store hiccup can delay persistence but never the revocation itself.
|
|
6475
|
+
*/
|
|
6476
|
+
/**
|
|
6477
|
+
* @durable class=ledger owner=local-auth
|
|
6478
|
+
* write="one row per login (password, passkey, SSO, handoff, session exchange);
|
|
6479
|
+
* rewritten on every refresh / renewal (expiry slides, refresh hash rotates)
|
|
6480
|
+
* and on revocation"
|
|
6481
|
+
* retention="the daily reap deletes rows expired more than 7 days ago and rows
|
|
6482
|
+
* revoked more than 30 days ago. A session the user keeps refreshing lives as
|
|
6483
|
+
* long as it is used."
|
|
6484
|
+
*/
|
|
6485
|
+
var LOGIN_SESSIONS_COLLECTION = "login_sessions";
|
|
6486
|
+
/**
|
|
6487
|
+
* @durable class=config owner=local-auth
|
|
6488
|
+
* write="revoking all of a user's sessions (logout everywhere, password change or
|
|
6489
|
+
* reset, user deletion) sets that user's cutoff to now"
|
|
6490
|
+
* retention="none — one row per user who ever had all sessions revoked; it only
|
|
6491
|
+
* ever matters for tokens minted before sessions existed, and is harmless after."
|
|
6492
|
+
*/
|
|
6493
|
+
var LOGIN_SESSION_CUTOFFS_COLLECTION = "login_session_cutoffs";
|
|
6494
|
+
var LOGIN_SESSIONS_COLUMNS = [
|
|
6495
|
+
{
|
|
6496
|
+
name: "id",
|
|
6497
|
+
type: "TEXT",
|
|
6498
|
+
primaryKey: true,
|
|
6499
|
+
notNull: true
|
|
6500
|
+
},
|
|
6501
|
+
{
|
|
6502
|
+
name: "userId",
|
|
6503
|
+
type: "TEXT",
|
|
6504
|
+
notNull: true
|
|
6505
|
+
},
|
|
6506
|
+
{
|
|
6507
|
+
name: "method",
|
|
6508
|
+
type: "TEXT",
|
|
6509
|
+
notNull: true
|
|
6510
|
+
},
|
|
6511
|
+
{
|
|
6512
|
+
name: "refreshable",
|
|
6513
|
+
type: "BOOLEAN",
|
|
6514
|
+
notNull: true
|
|
6515
|
+
},
|
|
6516
|
+
{
|
|
6517
|
+
name: "createdAt",
|
|
6518
|
+
type: "INTEGER",
|
|
6519
|
+
notNull: true
|
|
6520
|
+
},
|
|
6521
|
+
{
|
|
6522
|
+
name: "lastUsedAt",
|
|
6523
|
+
type: "INTEGER",
|
|
6524
|
+
notNull: true
|
|
6525
|
+
},
|
|
6526
|
+
{
|
|
6527
|
+
name: "expiresAt",
|
|
6528
|
+
type: "INTEGER",
|
|
6529
|
+
notNull: true
|
|
6530
|
+
},
|
|
6531
|
+
{
|
|
6532
|
+
name: "revokedAt",
|
|
6533
|
+
type: "INTEGER"
|
|
6534
|
+
},
|
|
6535
|
+
{
|
|
6536
|
+
name: "revokedReason",
|
|
6537
|
+
type: "TEXT"
|
|
6538
|
+
},
|
|
6539
|
+
{
|
|
6540
|
+
name: "refreshHash",
|
|
6541
|
+
type: "TEXT"
|
|
6542
|
+
},
|
|
6543
|
+
{
|
|
6544
|
+
name: "previousRefreshHash",
|
|
6545
|
+
type: "TEXT"
|
|
6546
|
+
},
|
|
6547
|
+
{
|
|
6548
|
+
name: "rotatedAt",
|
|
6549
|
+
type: "INTEGER"
|
|
6550
|
+
},
|
|
6551
|
+
{
|
|
6552
|
+
name: "client",
|
|
6553
|
+
type: "TEXT"
|
|
6554
|
+
},
|
|
6555
|
+
{
|
|
6556
|
+
name: "ip",
|
|
6557
|
+
type: "TEXT"
|
|
6558
|
+
}
|
|
6559
|
+
];
|
|
6560
|
+
var LOGIN_SESSION_CUTOFFS_COLUMNS = [{
|
|
6561
|
+
name: "userId",
|
|
6562
|
+
type: "TEXT",
|
|
6563
|
+
primaryKey: true,
|
|
6564
|
+
notNull: true
|
|
6565
|
+
}, {
|
|
6566
|
+
name: "notBefore",
|
|
6567
|
+
type: "INTEGER",
|
|
6568
|
+
notNull: true
|
|
6569
|
+
}];
|
|
6570
|
+
var METHODS = [
|
|
6571
|
+
"password",
|
|
6572
|
+
"passkey",
|
|
6573
|
+
"sso",
|
|
6574
|
+
"handoff",
|
|
6575
|
+
"exchange"
|
|
6576
|
+
];
|
|
6577
|
+
var REFRESH_TOKEN_PREFIX = "csr_";
|
|
6578
|
+
/**
|
|
6579
|
+
* A rotated-out refresh token is still honoured this long: two tabs (or a
|
|
6580
|
+
* retry after a dropped response) may present the same token a moment apart.
|
|
6581
|
+
* After it, the old token is evidence of a copy and ends the session.
|
|
6582
|
+
*/
|
|
6583
|
+
var REFRESH_REUSE_GRACE_MS = 3e4;
|
|
6584
|
+
var EXPIRED_RETENTION_MS = 7 * 864e5;
|
|
6585
|
+
var REVOKED_RETENTION_MS = 30 * 864e5;
|
|
6586
|
+
/** Longest client label kept (a User-Agent can be huge). */
|
|
6587
|
+
var MAX_CLIENT_LABEL = 200;
|
|
6588
|
+
function hashToken(token) {
|
|
6589
|
+
return node_crypto.createHash("sha256").update(token).digest("hex");
|
|
6590
|
+
}
|
|
6591
|
+
function str(data, key) {
|
|
6592
|
+
const value = data[key];
|
|
6593
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
6594
|
+
}
|
|
6595
|
+
function num(data, key) {
|
|
6596
|
+
const value = data[key];
|
|
6597
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
6598
|
+
}
|
|
6599
|
+
function parseSession$1(data) {
|
|
6600
|
+
const id = str(data, "id");
|
|
6601
|
+
const userId = str(data, "userId");
|
|
6602
|
+
const createdAt = num(data, "createdAt");
|
|
6603
|
+
const lastUsedAt = num(data, "lastUsedAt");
|
|
6604
|
+
const expiresAt = num(data, "expiresAt");
|
|
6605
|
+
const method = METHODS.find((m) => m === data["method"]);
|
|
6606
|
+
if (id === null || userId === null || createdAt === null || expiresAt === null) return null;
|
|
6607
|
+
return {
|
|
6608
|
+
id,
|
|
6609
|
+
userId,
|
|
6610
|
+
method: method ?? "password",
|
|
6611
|
+
refreshable: data["refreshable"] === true || data["refreshable"] === 1,
|
|
6612
|
+
createdAt,
|
|
6613
|
+
lastUsedAt: lastUsedAt ?? createdAt,
|
|
6614
|
+
expiresAt,
|
|
6615
|
+
revokedAt: num(data, "revokedAt"),
|
|
6616
|
+
revokedReason: str(data, "revokedReason"),
|
|
6617
|
+
refreshHash: str(data, "refreshHash"),
|
|
6618
|
+
previousRefreshHash: str(data, "previousRefreshHash"),
|
|
6619
|
+
rotatedAt: num(data, "rotatedAt"),
|
|
6620
|
+
client: str(data, "client"),
|
|
6621
|
+
ip: str(data, "ip")
|
|
6622
|
+
};
|
|
6623
|
+
}
|
|
6624
|
+
function toLoginSessionSummary(record) {
|
|
6625
|
+
const { refreshHash: _r, previousRefreshHash: _p, rotatedAt: _t, ...summary } = record;
|
|
6626
|
+
return summary;
|
|
6627
|
+
}
|
|
6628
|
+
var LoginSessionManager = class {
|
|
6629
|
+
store;
|
|
6630
|
+
sessions = /* @__PURE__ */ new Map();
|
|
6631
|
+
/** refresh hash (current or previous) → session id */
|
|
6632
|
+
byRefreshHash = /* @__PURE__ */ new Map();
|
|
6633
|
+
cutoffs = /* @__PURE__ */ new Map();
|
|
6634
|
+
now;
|
|
6635
|
+
logger;
|
|
6636
|
+
constructor(store, options = {}) {
|
|
6637
|
+
this.store = store;
|
|
6638
|
+
this.now = options.now ?? Date.now;
|
|
6639
|
+
this.logger = options.logger ?? null;
|
|
6640
|
+
}
|
|
6641
|
+
/** Load every row into the mirror. Call once at init, before serving. */
|
|
6642
|
+
async load() {
|
|
6643
|
+
const rows = await this.store.query.query({ collection: LOGIN_SESSIONS_COLLECTION });
|
|
6644
|
+
for (const row of rows) {
|
|
6645
|
+
const record = parseSession$1(row.data);
|
|
6646
|
+
if (record !== null) this.remember(record);
|
|
6647
|
+
}
|
|
6648
|
+
const cutoffs = await this.store.query.query({ collection: LOGIN_SESSION_CUTOFFS_COLLECTION });
|
|
6649
|
+
for (const row of cutoffs) {
|
|
6650
|
+
const userId = str(row.data, "userId");
|
|
6651
|
+
const notBefore = num(row.data, "notBefore");
|
|
6652
|
+
if (userId !== null && notBefore !== null) this.cutoffs.set(userId, notBefore);
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
async create(input) {
|
|
6656
|
+
const at = this.now();
|
|
6657
|
+
const refreshToken = input.refreshable ? this.newRefreshToken() : null;
|
|
6658
|
+
const session = {
|
|
6659
|
+
id: node_crypto.randomUUID(),
|
|
6660
|
+
userId: input.userId,
|
|
6661
|
+
method: input.method,
|
|
6662
|
+
refreshable: input.refreshable,
|
|
6663
|
+
createdAt: at,
|
|
6664
|
+
lastUsedAt: at,
|
|
6665
|
+
expiresAt: at + input.lifetimeMs,
|
|
6666
|
+
revokedAt: null,
|
|
6667
|
+
revokedReason: null,
|
|
6668
|
+
refreshHash: refreshToken === null ? null : hashToken(refreshToken),
|
|
6669
|
+
previousRefreshHash: null,
|
|
6670
|
+
rotatedAt: null,
|
|
6671
|
+
client: input.client === void 0 ? null : input.client.slice(0, MAX_CLIENT_LABEL),
|
|
6672
|
+
ip: input.ip ?? null
|
|
6673
|
+
};
|
|
6674
|
+
await this.store.insert.mutate({
|
|
6675
|
+
collection: LOGIN_SESSIONS_COLLECTION,
|
|
6676
|
+
record: {
|
|
6677
|
+
id: session.id,
|
|
6678
|
+
data: { ...session }
|
|
6679
|
+
}
|
|
6680
|
+
});
|
|
6681
|
+
this.remember(session);
|
|
6682
|
+
return {
|
|
6683
|
+
session,
|
|
6684
|
+
refreshToken
|
|
6685
|
+
};
|
|
6686
|
+
}
|
|
6687
|
+
/** Is `sessionId` a live session right now? Synchronous: the hub asks on every request. */
|
|
6688
|
+
check(sessionId) {
|
|
6689
|
+
const session = this.sessions.get(sessionId);
|
|
6690
|
+
if (session === void 0) return { state: "unknown" };
|
|
6691
|
+
if (session.revokedAt !== null) return { state: "revoked" };
|
|
6692
|
+
if (session.expiresAt <= this.now()) return { state: "expired" };
|
|
6693
|
+
return {
|
|
6694
|
+
state: "active",
|
|
6695
|
+
session
|
|
6696
|
+
};
|
|
6697
|
+
}
|
|
6698
|
+
/** Epoch ms before which a pre-session (no `sid`) token of `userId` is refused; 0 = none. */
|
|
6699
|
+
legacyCutoff(userId) {
|
|
6700
|
+
return this.cutoffs.get(userId) ?? 0;
|
|
6701
|
+
}
|
|
6702
|
+
/** Swap a refresh token for a rotated one, sliding the session's expiry to now + `lifetimeMs`. */
|
|
6703
|
+
async refresh(refreshToken, lifetimeMs) {
|
|
6704
|
+
if (!refreshToken.startsWith("csr_")) return {
|
|
6705
|
+
ok: false,
|
|
6706
|
+
reason: "unknown"
|
|
6707
|
+
};
|
|
6708
|
+
const presented = hashToken(refreshToken);
|
|
6709
|
+
const sessionId = this.byRefreshHash.get(presented);
|
|
6710
|
+
const session = sessionId === void 0 ? void 0 : this.sessions.get(sessionId);
|
|
6711
|
+
if (session === void 0) return {
|
|
6712
|
+
ok: false,
|
|
6713
|
+
reason: "unknown"
|
|
6714
|
+
};
|
|
6715
|
+
if (session.revokedAt !== null) return {
|
|
6716
|
+
ok: false,
|
|
6717
|
+
reason: "revoked"
|
|
6718
|
+
};
|
|
6719
|
+
const at = this.now();
|
|
6720
|
+
if (session.expiresAt <= at) return {
|
|
6721
|
+
ok: false,
|
|
6722
|
+
reason: "expired"
|
|
6723
|
+
};
|
|
6724
|
+
if (presented !== session.refreshHash) {
|
|
6725
|
+
if (!(presented === session.previousRefreshHash && session.rotatedAt !== null && at - session.rotatedAt <= REFRESH_REUSE_GRACE_MS)) {
|
|
6726
|
+
await this.revoke(session.id, "refresh-reuse");
|
|
6727
|
+
this.logger?.warn("A rotated-out refresh token was presented — session revoked", { meta: {
|
|
6728
|
+
sessionId: session.id,
|
|
6729
|
+
userId: session.userId
|
|
6730
|
+
} });
|
|
6731
|
+
return {
|
|
6732
|
+
ok: false,
|
|
6733
|
+
reason: "reused"
|
|
6734
|
+
};
|
|
6735
|
+
}
|
|
6736
|
+
}
|
|
6737
|
+
const next = this.newRefreshToken();
|
|
6738
|
+
const updated = {
|
|
6739
|
+
...session,
|
|
6740
|
+
lastUsedAt: at,
|
|
6741
|
+
expiresAt: at + lifetimeMs,
|
|
6742
|
+
previousRefreshHash: session.refreshHash,
|
|
6743
|
+
refreshHash: hashToken(next),
|
|
6744
|
+
rotatedAt: at
|
|
6745
|
+
};
|
|
6746
|
+
this.replace(session, updated);
|
|
6747
|
+
await this.persist(updated);
|
|
6748
|
+
return {
|
|
6749
|
+
ok: true,
|
|
6750
|
+
session: updated,
|
|
6751
|
+
refreshToken: next
|
|
6752
|
+
};
|
|
6753
|
+
}
|
|
6754
|
+
/** Renew an active session (legacy client): slide its expiry. `null` when it is not active. */
|
|
6755
|
+
async extend(sessionId, lifetimeMs) {
|
|
6756
|
+
const check = this.check(sessionId);
|
|
6757
|
+
if (check.state !== "active") return null;
|
|
6758
|
+
const at = this.now();
|
|
6759
|
+
const updated = {
|
|
6760
|
+
...check.session,
|
|
6761
|
+
lastUsedAt: at,
|
|
6762
|
+
expiresAt: at + lifetimeMs
|
|
6763
|
+
};
|
|
6764
|
+
this.replace(check.session, updated);
|
|
6765
|
+
await this.persist(updated);
|
|
6766
|
+
return updated;
|
|
6767
|
+
}
|
|
6768
|
+
/** End one session. `false` when it was unknown or already ended. */
|
|
6769
|
+
async revoke(sessionId, reason) {
|
|
6770
|
+
const session = this.sessions.get(sessionId);
|
|
6771
|
+
if (session === void 0 || session.revokedAt !== null) return false;
|
|
6772
|
+
const updated = {
|
|
6773
|
+
...session,
|
|
6774
|
+
revokedAt: this.now(),
|
|
6775
|
+
revokedReason: reason
|
|
6776
|
+
};
|
|
6777
|
+
this.replace(session, updated);
|
|
6778
|
+
await this.persist(updated);
|
|
6779
|
+
return true;
|
|
6780
|
+
}
|
|
6781
|
+
/**
|
|
6782
|
+
* End every live session of `userId` except `exceptSessionId`, and move the
|
|
6783
|
+
* user's legacy cutoff to now (pre-session tokens die too). Returns how many
|
|
6784
|
+
* sessions ended.
|
|
6785
|
+
*/
|
|
6786
|
+
async revokeAllForUser(userId, options) {
|
|
6787
|
+
const at = this.now();
|
|
6788
|
+
this.cutoffs.set(userId, at);
|
|
6789
|
+
const ended = [];
|
|
6790
|
+
for (const session of this.sessions.values()) {
|
|
6791
|
+
if (session.userId !== userId || session.revokedAt !== null) continue;
|
|
6792
|
+
if (session.id === options.exceptSessionId) continue;
|
|
6793
|
+
const updated = {
|
|
6794
|
+
...session,
|
|
6795
|
+
revokedAt: at,
|
|
6796
|
+
revokedReason: options.reason
|
|
6797
|
+
};
|
|
6798
|
+
this.replace(session, updated);
|
|
6799
|
+
ended.push(updated);
|
|
6800
|
+
}
|
|
6801
|
+
await this.persistCutoff(userId, at);
|
|
6802
|
+
for (const session of ended) await this.persist(session);
|
|
6803
|
+
return ended.length;
|
|
6804
|
+
}
|
|
6805
|
+
/** `userId`'s live sessions, newest first. */
|
|
6806
|
+
listForUser(userId) {
|
|
6807
|
+
return this.listActive().filter((s) => s.userId === userId);
|
|
6808
|
+
}
|
|
6809
|
+
/** Every live session, newest first. */
|
|
6810
|
+
listActive() {
|
|
6811
|
+
const at = this.now();
|
|
6812
|
+
return [...this.sessions.values()].filter((s) => s.revokedAt === null && s.expiresAt > at).toSorted((a, b) => b.createdAt - a.createdAt).map(toLoginSessionSummary);
|
|
6813
|
+
}
|
|
6814
|
+
/** Delete rows expired more than 7 days ago or revoked more than 30. Returns how many. */
|
|
6815
|
+
async reap() {
|
|
6816
|
+
const at = this.now();
|
|
6817
|
+
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);
|
|
6818
|
+
for (const session of dead) {
|
|
6819
|
+
this.forget(session);
|
|
6820
|
+
await this.store.delete.mutate({
|
|
6821
|
+
collection: LOGIN_SESSIONS_COLLECTION,
|
|
6822
|
+
key: session.id
|
|
6823
|
+
});
|
|
6824
|
+
}
|
|
6825
|
+
return dead.length;
|
|
6826
|
+
}
|
|
6827
|
+
newRefreshToken() {
|
|
6828
|
+
return `${REFRESH_TOKEN_PREFIX}${node_crypto.randomBytes(32).toString("base64url")}`;
|
|
6829
|
+
}
|
|
6830
|
+
remember(session) {
|
|
6831
|
+
this.sessions.set(session.id, session);
|
|
6832
|
+
if (session.refreshHash !== null) this.byRefreshHash.set(session.refreshHash, session.id);
|
|
6833
|
+
if (session.previousRefreshHash !== null) this.byRefreshHash.set(session.previousRefreshHash, session.id);
|
|
6834
|
+
}
|
|
6835
|
+
forget(session) {
|
|
6836
|
+
this.sessions.delete(session.id);
|
|
6837
|
+
if (session.refreshHash !== null) this.byRefreshHash.delete(session.refreshHash);
|
|
6838
|
+
if (session.previousRefreshHash !== null) this.byRefreshHash.delete(session.previousRefreshHash);
|
|
6839
|
+
}
|
|
6840
|
+
replace(previous, next) {
|
|
6841
|
+
this.forget(previous);
|
|
6842
|
+
this.remember(next);
|
|
6843
|
+
}
|
|
6844
|
+
async persist(session) {
|
|
6845
|
+
try {
|
|
6846
|
+
await this.store.update.mutate({
|
|
6847
|
+
collection: LOGIN_SESSIONS_COLLECTION,
|
|
6848
|
+
id: session.id,
|
|
6849
|
+
data: { ...session }
|
|
6850
|
+
});
|
|
6851
|
+
} catch (error) {
|
|
6852
|
+
this.logger?.error("Login session write failed — the change holds until restart", { meta: {
|
|
6853
|
+
sessionId: session.id,
|
|
6854
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6855
|
+
} });
|
|
6856
|
+
}
|
|
6857
|
+
}
|
|
6858
|
+
async persistCutoff(userId, notBefore) {
|
|
6859
|
+
try {
|
|
6860
|
+
await this.store.delete.mutate({
|
|
6861
|
+
collection: LOGIN_SESSION_CUTOFFS_COLLECTION,
|
|
6862
|
+
key: userId
|
|
6863
|
+
});
|
|
6864
|
+
await this.store.insert.mutate({
|
|
6865
|
+
collection: LOGIN_SESSION_CUTOFFS_COLLECTION,
|
|
6866
|
+
record: {
|
|
6867
|
+
id: userId,
|
|
6868
|
+
data: {
|
|
6869
|
+
userId,
|
|
6870
|
+
notBefore
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
});
|
|
6874
|
+
} catch (error) {
|
|
6875
|
+
this.logger?.error("Login session cutoff write failed — it holds until restart", { meta: {
|
|
6876
|
+
userId,
|
|
6877
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6878
|
+
} });
|
|
6879
|
+
}
|
|
6880
|
+
}
|
|
6881
|
+
};
|
|
6882
|
+
//#endregion
|
|
6883
|
+
//#region src/auth/admin-token-manager.ts
|
|
6884
|
+
/**
|
|
6885
|
+
* Admin tokens (`csa_…`) — the one credential an ops script can hold that
|
|
6886
|
+
* acts as an administrator, minted deliberately and kept on a short leash.
|
|
6887
|
+
*
|
|
6888
|
+
* Scoped tokens (`cst_`) are never admin, and a script that needs admin (the
|
|
6889
|
+
* keychain migration, a release's `applyServerUpdate`) otherwise needs a human
|
|
6890
|
+
* login. This is that credential, with the rules the operator set:
|
|
6891
|
+
*
|
|
6892
|
+
* - created only by an admin, from the UI, after re-authentication (the hub
|
|
6893
|
+
* router enforces both);
|
|
6894
|
+
* - expiry MANDATORY, at most 90 days;
|
|
6895
|
+
* - shown once — only a SHA-256 hash is stored, like `cst_`;
|
|
6896
|
+
* - every use audit-logged (the hub, per request);
|
|
6897
|
+
* - revocable, and dead the moment its creator is deleted or loses admin;
|
|
6898
|
+
* - NOT a session: an opaque token, so it survives a signing-key change.
|
|
6899
|
+
*/
|
|
6900
|
+
/**
|
|
6901
|
+
* @durable class=config owner=local-auth
|
|
6902
|
+
* write="an admin creates a token (after re-authentication); `lastUsedAt` is
|
|
6903
|
+
* rewritten at most once a minute while it is used; `revoke` stamps revokedAt"
|
|
6904
|
+
* retention="the daily reap deletes rows expired or revoked more than 30 days ago"
|
|
6905
|
+
*/
|
|
6906
|
+
var ADMIN_TOKENS_COLLECTION = "admin_tokens";
|
|
6907
|
+
var ADMIN_TOKENS_COLUMNS = [
|
|
6908
|
+
{
|
|
6909
|
+
name: "id",
|
|
6910
|
+
type: "TEXT",
|
|
6911
|
+
primaryKey: true,
|
|
6912
|
+
notNull: true
|
|
6913
|
+
},
|
|
6914
|
+
{
|
|
6915
|
+
name: "createdBy",
|
|
6916
|
+
type: "TEXT",
|
|
6917
|
+
notNull: true
|
|
6918
|
+
},
|
|
6919
|
+
{
|
|
6920
|
+
name: "name",
|
|
6921
|
+
type: "TEXT",
|
|
6922
|
+
notNull: true
|
|
6923
|
+
},
|
|
6924
|
+
{
|
|
6925
|
+
name: "tokenHash",
|
|
6926
|
+
type: "TEXT",
|
|
6927
|
+
notNull: true,
|
|
6928
|
+
unique: true
|
|
6929
|
+
},
|
|
6930
|
+
{
|
|
6931
|
+
name: "tokenPrefix",
|
|
6932
|
+
type: "TEXT",
|
|
6933
|
+
notNull: true
|
|
6934
|
+
},
|
|
6935
|
+
{
|
|
6936
|
+
name: "createdAt",
|
|
6937
|
+
type: "INTEGER",
|
|
6938
|
+
notNull: true
|
|
6939
|
+
},
|
|
6940
|
+
{
|
|
6941
|
+
name: "expiresAt",
|
|
6942
|
+
type: "INTEGER",
|
|
6943
|
+
notNull: true
|
|
6944
|
+
},
|
|
6945
|
+
{
|
|
6946
|
+
name: "lastUsedAt",
|
|
6947
|
+
type: "INTEGER"
|
|
6948
|
+
},
|
|
6949
|
+
{
|
|
6950
|
+
name: "revokedAt",
|
|
6951
|
+
type: "INTEGER"
|
|
6952
|
+
}
|
|
6953
|
+
];
|
|
6954
|
+
var ADMIN_TOKEN_PREFIX = "csa_";
|
|
6955
|
+
var ADMIN_TOKEN_MAX_LIFETIME_MS = 90 * 864e5;
|
|
6956
|
+
var DEAD_RETENTION_MS = 30 * 864e5;
|
|
6957
|
+
var TOUCH_EVERY_MS = 6e4;
|
|
6958
|
+
function hash(token) {
|
|
6959
|
+
return node_crypto.createHash("sha256").update(token).digest("hex");
|
|
6960
|
+
}
|
|
6961
|
+
function parse(data) {
|
|
6962
|
+
const { id, createdBy, name, tokenHash, tokenPrefix, createdAt, expiresAt } = data;
|
|
6963
|
+
if (typeof id !== "string" || typeof createdBy !== "string" || typeof name !== "string" || typeof tokenHash !== "string" || typeof tokenPrefix !== "string" || typeof createdAt !== "number" || typeof expiresAt !== "number") return null;
|
|
6964
|
+
return {
|
|
6965
|
+
id,
|
|
6966
|
+
createdBy,
|
|
6967
|
+
name,
|
|
6968
|
+
tokenHash,
|
|
6969
|
+
tokenPrefix,
|
|
6970
|
+
createdAt,
|
|
6971
|
+
expiresAt,
|
|
6972
|
+
lastUsedAt: typeof data["lastUsedAt"] === "number" ? data["lastUsedAt"] : null,
|
|
6973
|
+
revokedAt: typeof data["revokedAt"] === "number" ? data["revokedAt"] : null
|
|
6974
|
+
};
|
|
6975
|
+
}
|
|
6976
|
+
function summary(stored) {
|
|
6977
|
+
const { tokenHash: _hash, ...record } = stored;
|
|
6978
|
+
return record;
|
|
6979
|
+
}
|
|
6980
|
+
var AdminTokenManager = class {
|
|
6981
|
+
store;
|
|
6982
|
+
now;
|
|
6983
|
+
constructor(store, now = Date.now) {
|
|
6984
|
+
this.store = store;
|
|
6985
|
+
this.now = now;
|
|
6986
|
+
}
|
|
6987
|
+
/** Mint a token for `createdBy`. Refuses an expiry in the past or past 90 days. */
|
|
6988
|
+
async create(input) {
|
|
6989
|
+
const at = this.now();
|
|
6990
|
+
if (!(input.expiresAt > at) || input.expiresAt > at + 7776e6) throw new Error("An admin token must expire within 90 days");
|
|
6991
|
+
const name = input.name.trim().slice(0, 80);
|
|
6992
|
+
if (name.length === 0) throw new Error("An admin token needs a name");
|
|
6993
|
+
const token = `${ADMIN_TOKEN_PREFIX}${node_crypto.randomBytes(32).toString("base64url")}`;
|
|
6994
|
+
const stored = {
|
|
6995
|
+
id: node_crypto.randomUUID(),
|
|
6996
|
+
createdBy: input.createdBy,
|
|
6997
|
+
name,
|
|
6998
|
+
tokenHash: hash(token),
|
|
6999
|
+
tokenPrefix: token.slice(0, 12),
|
|
7000
|
+
createdAt: at,
|
|
7001
|
+
expiresAt: input.expiresAt,
|
|
7002
|
+
lastUsedAt: null,
|
|
7003
|
+
revokedAt: null
|
|
7004
|
+
};
|
|
7005
|
+
await this.store.insert.mutate({
|
|
7006
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7007
|
+
record: {
|
|
7008
|
+
id: stored.id,
|
|
7009
|
+
data: { ...stored }
|
|
7010
|
+
}
|
|
7011
|
+
});
|
|
7012
|
+
return {
|
|
7013
|
+
token,
|
|
7014
|
+
record: summary(stored)
|
|
7015
|
+
};
|
|
7016
|
+
}
|
|
7017
|
+
/** The live record behind `token`, or `null` (unknown, revoked, expired, not a `csa_`). */
|
|
7018
|
+
async validate(token) {
|
|
7019
|
+
if (!token.startsWith("csa_")) return null;
|
|
7020
|
+
const first = (await this.store.query.query({
|
|
7021
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7022
|
+
filter: { where: { tokenHash: hash(token) } }
|
|
7023
|
+
}))[0];
|
|
7024
|
+
const stored = first === void 0 ? null : parse(first.data);
|
|
7025
|
+
if (stored === null || stored.revokedAt !== null) return null;
|
|
7026
|
+
const at = this.now();
|
|
7027
|
+
if (stored.expiresAt <= at) return null;
|
|
7028
|
+
if (stored.lastUsedAt === null || at - stored.lastUsedAt > TOUCH_EVERY_MS) this.store.update.mutate({
|
|
7029
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7030
|
+
id: stored.id,
|
|
7031
|
+
data: {
|
|
7032
|
+
...stored,
|
|
7033
|
+
lastUsedAt: at
|
|
7034
|
+
}
|
|
7035
|
+
}).catch(() => {});
|
|
7036
|
+
return summary(stored);
|
|
7037
|
+
}
|
|
7038
|
+
/** Every token not yet reaped, newest first (no hashes). */
|
|
7039
|
+
async list() {
|
|
7040
|
+
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);
|
|
7041
|
+
}
|
|
7042
|
+
/** Revoke one token. `false` when unknown or already revoked. */
|
|
7043
|
+
async revoke(id) {
|
|
7044
|
+
const first = (await this.store.query.query({
|
|
7045
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7046
|
+
filter: { where: { id } }
|
|
7047
|
+
}))[0];
|
|
7048
|
+
const stored = first === void 0 ? null : parse(first.data);
|
|
7049
|
+
if (stored === null || stored.revokedAt !== null) return false;
|
|
7050
|
+
await this.store.update.mutate({
|
|
7051
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7052
|
+
id,
|
|
7053
|
+
data: {
|
|
7054
|
+
...stored,
|
|
7055
|
+
revokedAt: this.now()
|
|
7056
|
+
}
|
|
7057
|
+
});
|
|
7058
|
+
return true;
|
|
7059
|
+
}
|
|
7060
|
+
/** Revoke every live token `userId` created (the user was deleted). */
|
|
7061
|
+
async revokeAllCreatedBy(userId) {
|
|
7062
|
+
let revoked = 0;
|
|
7063
|
+
for (const token of await this.list()) if (token.createdBy === userId && token.revokedAt === null) {
|
|
7064
|
+
if (await this.revoke(token.id)) revoked++;
|
|
7065
|
+
}
|
|
7066
|
+
return revoked;
|
|
7067
|
+
}
|
|
7068
|
+
/** Delete rows expired or revoked more than 30 days ago. */
|
|
7069
|
+
async reap() {
|
|
7070
|
+
const horizon = this.now() - DEAD_RETENTION_MS;
|
|
7071
|
+
let deleted = 0;
|
|
7072
|
+
for (const token of await this.list()) if ((token.revokedAt ?? token.expiresAt) < horizon) {
|
|
7073
|
+
await this.store.delete.mutate({
|
|
7074
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7075
|
+
key: token.id
|
|
7076
|
+
});
|
|
7077
|
+
deleted++;
|
|
7078
|
+
}
|
|
7079
|
+
return deleted;
|
|
7080
|
+
}
|
|
7081
|
+
};
|
|
7082
|
+
//#endregion
|
|
7083
|
+
//#region src/auth/auth-audit-log.ts
|
|
7084
|
+
/**
|
|
7085
|
+
* The authentication audit trail: what an operator must be able to answer
|
|
7086
|
+
* after the fact — "who skipped 2FA from the LAN, and from where?", "which
|
|
7087
|
+
* admin token ran this, and when?".
|
|
7088
|
+
*
|
|
7089
|
+
* One row per event, kept 90 days. Writing never throws: an audit row that
|
|
7090
|
+
* cannot be stored is logged loudly instead, and never blocks the action
|
|
7091
|
+
* (a login must not fail because the audit table is busy).
|
|
7092
|
+
*/
|
|
7093
|
+
/**
|
|
7094
|
+
* @durable class=ledger owner=local-auth
|
|
7095
|
+
* write="one row per audited auth event: a second factor skipped from a trusted
|
|
7096
|
+
* network, an admin token created / used / revoked"
|
|
7097
|
+
* retention="the daily reap deletes rows older than 90 days"
|
|
7098
|
+
*/
|
|
7099
|
+
var AUTH_AUDIT_COLLECTION = "auth_audit";
|
|
7100
|
+
var AUTH_AUDIT_COLUMNS = [
|
|
7101
|
+
{
|
|
7102
|
+
name: "id",
|
|
7103
|
+
type: "TEXT",
|
|
7104
|
+
primaryKey: true,
|
|
7105
|
+
notNull: true
|
|
7106
|
+
},
|
|
7107
|
+
{
|
|
7108
|
+
name: "at",
|
|
7109
|
+
type: "INTEGER",
|
|
7110
|
+
notNull: true
|
|
7111
|
+
},
|
|
7112
|
+
{
|
|
7113
|
+
name: "action",
|
|
7114
|
+
type: "TEXT",
|
|
7115
|
+
notNull: true
|
|
7116
|
+
},
|
|
7117
|
+
{
|
|
7118
|
+
name: "userId",
|
|
7119
|
+
type: "TEXT"
|
|
7120
|
+
},
|
|
7121
|
+
{
|
|
7122
|
+
name: "ip",
|
|
7123
|
+
type: "TEXT"
|
|
7124
|
+
},
|
|
7125
|
+
{
|
|
7126
|
+
name: "detail",
|
|
7127
|
+
type: "TEXT"
|
|
7128
|
+
}
|
|
7129
|
+
];
|
|
7130
|
+
var AUTH_AUDIT_ACTIONS = [
|
|
7131
|
+
"second-factor-skipped",
|
|
7132
|
+
"admin-token-created",
|
|
7133
|
+
"admin-token-used",
|
|
7134
|
+
"admin-token-revoked"
|
|
7135
|
+
];
|
|
7136
|
+
var RETENTION_MS = 90 * 864e5;
|
|
7137
|
+
var MAX_LIST = 500;
|
|
7138
|
+
function parseRecord(data) {
|
|
7139
|
+
const id = data["id"];
|
|
7140
|
+
const at = data["at"];
|
|
7141
|
+
const action = data["action"];
|
|
7142
|
+
const known = AUTH_AUDIT_ACTIONS.find((a) => a === action);
|
|
7143
|
+
if (typeof id !== "string" || typeof at !== "number" || known === void 0) return null;
|
|
7144
|
+
let detail;
|
|
7145
|
+
if (typeof data["detail"] === "string") try {
|
|
7146
|
+
const parsed = JSON.parse(data["detail"]);
|
|
7147
|
+
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"));
|
|
7148
|
+
} catch {
|
|
7149
|
+
detail = void 0;
|
|
7150
|
+
}
|
|
7151
|
+
return {
|
|
7152
|
+
id,
|
|
7153
|
+
at,
|
|
7154
|
+
action: known,
|
|
7155
|
+
...typeof data["userId"] === "string" ? { userId: data["userId"] } : {},
|
|
7156
|
+
...typeof data["ip"] === "string" ? { ip: data["ip"] } : {},
|
|
7157
|
+
...detail !== void 0 ? { detail } : {}
|
|
7158
|
+
};
|
|
7159
|
+
}
|
|
7160
|
+
var AuthAuditLog = class {
|
|
7161
|
+
store;
|
|
7162
|
+
logger;
|
|
7163
|
+
now;
|
|
7164
|
+
constructor(store, logger = null, now = Date.now) {
|
|
7165
|
+
this.store = store;
|
|
7166
|
+
this.logger = logger;
|
|
7167
|
+
this.now = now;
|
|
7168
|
+
}
|
|
7169
|
+
/** Record `event`. Never throws. */
|
|
7170
|
+
async record(event) {
|
|
7171
|
+
const row = {
|
|
7172
|
+
id: node_crypto.randomUUID(),
|
|
7173
|
+
at: this.now(),
|
|
7174
|
+
action: event.action,
|
|
7175
|
+
userId: event.userId ?? null,
|
|
7176
|
+
ip: event.ip ?? null,
|
|
7177
|
+
detail: event.detail === void 0 ? null : JSON.stringify(event.detail)
|
|
7178
|
+
};
|
|
7179
|
+
this.logger?.info(`auth audit: ${event.action}`, { meta: {
|
|
7180
|
+
userId: event.userId,
|
|
7181
|
+
ip: event.ip,
|
|
7182
|
+
...event.detail
|
|
7183
|
+
} });
|
|
7184
|
+
try {
|
|
7185
|
+
await this.store.insert.mutate({
|
|
7186
|
+
collection: AUTH_AUDIT_COLLECTION,
|
|
7187
|
+
record: {
|
|
7188
|
+
id: row.id,
|
|
7189
|
+
data: row
|
|
7190
|
+
}
|
|
7191
|
+
});
|
|
7192
|
+
} catch (error) {
|
|
7193
|
+
this.logger?.error("auth audit row not written", { meta: {
|
|
7194
|
+
action: event.action,
|
|
7195
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7196
|
+
} });
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
7199
|
+
/** Newest first, at most `limit` (≤ 500), optionally one user's. */
|
|
7200
|
+
async list(input = {}) {
|
|
7201
|
+
const rows = await this.store.query.query({
|
|
7202
|
+
collection: AUTH_AUDIT_COLLECTION,
|
|
7203
|
+
...input.userId !== void 0 ? { filter: { where: { userId: input.userId } } } : {}
|
|
7204
|
+
});
|
|
7205
|
+
const limit = Math.min(MAX_LIST, Math.max(1, input.limit ?? 100));
|
|
7206
|
+
return rows.map((row) => parseRecord(row.data)).filter((r) => r !== null).toSorted((a, b) => b.at - a.at).slice(0, limit);
|
|
7207
|
+
}
|
|
7208
|
+
/** Delete rows older than 90 days. Returns how many. */
|
|
7209
|
+
async reap() {
|
|
7210
|
+
const horizon = this.now() - RETENTION_MS;
|
|
7211
|
+
const rows = await this.store.query.query({ collection: AUTH_AUDIT_COLLECTION });
|
|
7212
|
+
let deleted = 0;
|
|
7213
|
+
for (const row of rows) {
|
|
7214
|
+
const at = row.data["at"];
|
|
7215
|
+
if (typeof at === "number" && at < horizon) {
|
|
7216
|
+
await this.store.delete.mutate({
|
|
7217
|
+
collection: AUTH_AUDIT_COLLECTION,
|
|
7218
|
+
key: String(row.id)
|
|
7219
|
+
});
|
|
7220
|
+
deleted++;
|
|
7221
|
+
}
|
|
7222
|
+
}
|
|
7223
|
+
return deleted;
|
|
7224
|
+
}
|
|
7225
|
+
};
|
|
7226
|
+
//#endregion
|
|
7227
|
+
//#region src/auth/user-management-internal.ts
|
|
7228
|
+
/** Process-global, so every bundled copy of this module agrees on the key. */
|
|
7229
|
+
var USER_MANAGEMENT_INTERNAL = Symbol.for("camstack.user-management.internal");
|
|
7230
|
+
/** Attach `internal` to `provider` (returns the same object, now carrying it). */
|
|
7231
|
+
function attachUserManagementInternal(provider, internal) {
|
|
7232
|
+
Object.defineProperty(provider, USER_MANAGEMENT_INTERNAL, {
|
|
7233
|
+
value: internal,
|
|
7234
|
+
enumerable: false,
|
|
7235
|
+
writable: false,
|
|
7236
|
+
configurable: false
|
|
7237
|
+
});
|
|
7238
|
+
return provider;
|
|
7239
|
+
}
|
|
7240
|
+
/** The hub-internal surface of a `user-management` provider, or `null` when it carries none. */
|
|
7241
|
+
function getUserManagementInternal(provider) {
|
|
7242
|
+
if (provider === null || typeof provider !== "object") return null;
|
|
7243
|
+
const internal = Reflect.get(provider, USER_MANAGEMENT_INTERNAL);
|
|
7244
|
+
if (internal === null || typeof internal !== "object") return null;
|
|
7245
|
+
return internal;
|
|
7246
|
+
}
|
|
7247
|
+
//#endregion
|
|
6623
7248
|
//#region src/auth/user-manager.ts
|
|
6624
7249
|
var USERS_COLLECTION$1 = "users";
|
|
6625
7250
|
/**
|
|
@@ -6631,7 +7256,7 @@ var USERS_COLLECTION$1 = "users";
|
|
|
6631
7256
|
var FIRST_BOOT_ADMIN_USERNAME = "admin";
|
|
6632
7257
|
var FIRST_BOOT_ADMIN_PASSWORD = "changeme";
|
|
6633
7258
|
function parseUser(data) {
|
|
6634
|
-
return parseRecord("user", require_dist.UserRecordSchema, data);
|
|
7259
|
+
return parseRecord$1("user", require_dist.UserRecordSchema, data);
|
|
6635
7260
|
}
|
|
6636
7261
|
var UserManager = class {
|
|
6637
7262
|
storageAccess;
|
|
@@ -7205,6 +7830,34 @@ async function declareAuthSchema(store) {
|
|
|
7205
7830
|
columns: ["userId"]
|
|
7206
7831
|
}]
|
|
7207
7832
|
});
|
|
7833
|
+
await store.declareCollection.mutate({
|
|
7834
|
+
collection: LOGIN_SESSIONS_COLLECTION,
|
|
7835
|
+
columns: LOGIN_SESSIONS_COLUMNS,
|
|
7836
|
+
indexes: [{
|
|
7837
|
+
name: "idx_login_sessions_user_id",
|
|
7838
|
+
columns: ["userId"]
|
|
7839
|
+
}]
|
|
7840
|
+
});
|
|
7841
|
+
await store.declareCollection.mutate({
|
|
7842
|
+
collection: LOGIN_SESSION_CUTOFFS_COLLECTION,
|
|
7843
|
+
columns: LOGIN_SESSION_CUTOFFS_COLUMNS
|
|
7844
|
+
});
|
|
7845
|
+
await store.declareCollection.mutate({
|
|
7846
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7847
|
+
columns: ADMIN_TOKENS_COLUMNS,
|
|
7848
|
+
indexes: [{
|
|
7849
|
+
name: "idx_admin_tokens_created_by",
|
|
7850
|
+
columns: ["createdBy"]
|
|
7851
|
+
}]
|
|
7852
|
+
});
|
|
7853
|
+
await store.declareCollection.mutate({
|
|
7854
|
+
collection: AUTH_AUDIT_COLLECTION,
|
|
7855
|
+
columns: AUTH_AUDIT_COLUMNS,
|
|
7856
|
+
indexes: [{
|
|
7857
|
+
name: "idx_auth_audit_user_id",
|
|
7858
|
+
columns: ["userId"]
|
|
7859
|
+
}]
|
|
7860
|
+
});
|
|
7208
7861
|
}
|
|
7209
7862
|
//#endregion
|
|
7210
7863
|
//#region src/auth/external-identity-manager.ts
|
|
@@ -10078,6 +10731,9 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
10078
10731
|
totpManager = null;
|
|
10079
10732
|
externalIdentities = null;
|
|
10080
10733
|
signingKeyKeeper = null;
|
|
10734
|
+
loginSessions = null;
|
|
10735
|
+
adminTokens = null;
|
|
10736
|
+
auditLog = null;
|
|
10081
10737
|
constructor() {
|
|
10082
10738
|
super({});
|
|
10083
10739
|
}
|
|
@@ -10098,11 +10754,15 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
10098
10754
|
return store;
|
|
10099
10755
|
} };
|
|
10100
10756
|
this.userManager = new UserManager(storageAccess, this.authManager);
|
|
10101
|
-
this.apiKeyManager = new ApiKeyManager(storageAccess
|
|
10757
|
+
this.apiKeyManager = new ApiKeyManager(storageAccess);
|
|
10102
10758
|
this.scopedTokenManager = new ScopedTokenManager(store);
|
|
10103
10759
|
this.oauthSessionManager = new OauthSessionManager(store);
|
|
10104
10760
|
this.totpManager = new TotpManager(store);
|
|
10105
10761
|
this.externalIdentities = new ExternalIdentityManager(store);
|
|
10762
|
+
this.loginSessions = new LoginSessionManager(store, { logger: this.ctx.logger });
|
|
10763
|
+
await this.loginSessions.load();
|
|
10764
|
+
this.adminTokens = new AdminTokenManager(store);
|
|
10765
|
+
this.auditLog = new AuthAuditLog(store, this.ctx.logger);
|
|
10106
10766
|
try {
|
|
10107
10767
|
await reconcileAdminAccountAtBoot({
|
|
10108
10768
|
users: this.userManager,
|
|
@@ -10195,12 +10855,13 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
10195
10855
|
deleteUser: async (input) => {
|
|
10196
10856
|
if (!this.userManager) throw new Error("User management not available");
|
|
10197
10857
|
await this.userManager.delete(input.id);
|
|
10198
|
-
await this.
|
|
10858
|
+
await this.endAllAccess(input.id);
|
|
10199
10859
|
return { success: true };
|
|
10200
10860
|
},
|
|
10201
10861
|
resetPassword: async (input) => {
|
|
10202
10862
|
if (!this.userManager) throw new Error("User management not available");
|
|
10203
10863
|
await this.userManager.resetPassword(input.id, input.newPassword);
|
|
10864
|
+
await this.loginSessions?.revokeAllForUser(input.id, { reason: "password-reset" });
|
|
10204
10865
|
return { success: true };
|
|
10205
10866
|
},
|
|
10206
10867
|
setUserScopes: async (input) => {
|
|
@@ -10222,27 +10883,11 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
10222
10883
|
if (!this.apiKeyManager) return [];
|
|
10223
10884
|
return this.apiKeyManager.listAll();
|
|
10224
10885
|
},
|
|
10225
|
-
createApiKey: async (input) => {
|
|
10226
|
-
if (!this.apiKeyManager) throw new Error("API key management not available");
|
|
10227
|
-
const { record, token } = await this.apiKeyManager.create(input);
|
|
10228
|
-
const { tokenHash: _, ...summary } = record;
|
|
10229
|
-
return {
|
|
10230
|
-
token,
|
|
10231
|
-
record: summary
|
|
10232
|
-
};
|
|
10233
|
-
},
|
|
10234
10886
|
revokeApiKey: async (input) => {
|
|
10235
10887
|
if (!this.apiKeyManager) throw new Error("API key management not available");
|
|
10236
10888
|
await this.apiKeyManager.revoke(input.id);
|
|
10237
10889
|
return { success: true };
|
|
10238
10890
|
},
|
|
10239
|
-
validateApiKey: async (input) => {
|
|
10240
|
-
if (!this.apiKeyManager) return null;
|
|
10241
|
-
const record = await this.apiKeyManager.validateToken(input.token);
|
|
10242
|
-
if (!record) return null;
|
|
10243
|
-
const { tokenHash: _, ...summary } = record;
|
|
10244
|
-
return summary;
|
|
10245
|
-
},
|
|
10246
10891
|
createScopedToken: async (input) => {
|
|
10247
10892
|
if (!this.scopedTokenManager) throw new Error("Scoped token management not available");
|
|
10248
10893
|
const { token, record } = await this.scopedTokenManager.create(input.userId, input.name, input.scopes, input.expiresAt);
|
|
@@ -10257,8 +10902,10 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
10257
10902
|
return { success: true };
|
|
10258
10903
|
},
|
|
10259
10904
|
validateScopedToken: async (input) => {
|
|
10260
|
-
if (!this.scopedTokenManager) return null;
|
|
10261
|
-
|
|
10905
|
+
if (!this.scopedTokenManager || !this.userManager) return null;
|
|
10906
|
+
const record = await this.scopedTokenManager.validate(input.token);
|
|
10907
|
+
if (record === null) return null;
|
|
10908
|
+
return await this.userManager.findById(record.userId) === null ? null : record;
|
|
10262
10909
|
},
|
|
10263
10910
|
listScopedTokens: async (input) => {
|
|
10264
10911
|
if (!this.scopedTokenManager) return [];
|
|
@@ -10301,12 +10948,6 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
10301
10948
|
issuer: link.issuer
|
|
10302
10949
|
};
|
|
10303
10950
|
},
|
|
10304
|
-
linkExternalIdentity: async (input) => {
|
|
10305
|
-
if (!this.externalIdentities || !this.userManager) throw new Error("External sign-in linking not available");
|
|
10306
|
-
if (!await this.userManager.findById(input.userId)) throw new Error(`User not found: ${input.userId}`);
|
|
10307
|
-
await this.externalIdentities.link(input);
|
|
10308
|
-
return { success: true };
|
|
10309
|
-
},
|
|
10310
10951
|
unlinkExternalIdentity: async (input) => {
|
|
10311
10952
|
if (!this.externalIdentities) return { success: false };
|
|
10312
10953
|
return { success: await this.externalIdentities.unlink(input) };
|
|
@@ -10344,6 +10985,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
10344
10985
|
}
|
|
10345
10986
|
};
|
|
10346
10987
|
this.ctx.logger.info("registered auth-provider + user-management capabilities");
|
|
10988
|
+
attachUserManagementInternal(userMgmt, this.buildInternal());
|
|
10347
10989
|
return [{
|
|
10348
10990
|
capability: require_dist.authProviderCapability,
|
|
10349
10991
|
provider: authProvider
|
|
@@ -10353,6 +10995,93 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
10353
10995
|
}];
|
|
10354
10996
|
}
|
|
10355
10997
|
/**
|
|
10998
|
+
* The hub-internal surface (`user-management-internal.ts`): operations no
|
|
10999
|
+
* wire may reach. Called only by hub code holding this provider object.
|
|
11000
|
+
*/
|
|
11001
|
+
buildInternal() {
|
|
11002
|
+
const sessions = () => {
|
|
11003
|
+
if (this.loginSessions === null) throw new Error("Login sessions not available");
|
|
11004
|
+
return this.loginSessions;
|
|
11005
|
+
};
|
|
11006
|
+
return {
|
|
11007
|
+
sessions: {
|
|
11008
|
+
create: (input) => sessions().create(input),
|
|
11009
|
+
refresh: (token, lifetimeMs) => sessions().refresh(token, lifetimeMs),
|
|
11010
|
+
extend: (id, lifetimeMs) => sessions().extend(id, lifetimeMs),
|
|
11011
|
+
check: (id) => sessions().check(id),
|
|
11012
|
+
legacyCutoff: (userId) => sessions().legacyCutoff(userId),
|
|
11013
|
+
revoke: (id, reason) => sessions().revoke(id, reason),
|
|
11014
|
+
revokeAllForUser: (userId, options) => sessions().revokeAllForUser(userId, options),
|
|
11015
|
+
listForUser: (userId) => sessions().listForUser(userId),
|
|
11016
|
+
listActive: () => sessions().listActive()
|
|
11017
|
+
},
|
|
11018
|
+
adminTokens: {
|
|
11019
|
+
create: async (input) => {
|
|
11020
|
+
if (this.adminTokens === null) throw new Error("Admin tokens not available");
|
|
11021
|
+
return this.adminTokens.create(input);
|
|
11022
|
+
},
|
|
11023
|
+
validate: async (token) => {
|
|
11024
|
+
if (this.adminTokens === null || this.userManager === null) return null;
|
|
11025
|
+
const record = await this.adminTokens.validate(token);
|
|
11026
|
+
if (record === null) return null;
|
|
11027
|
+
const creator = await this.userManager.findById(record.createdBy);
|
|
11028
|
+
return creator !== null && creator.isAdmin ? record : null;
|
|
11029
|
+
},
|
|
11030
|
+
list: async () => this.adminTokens === null ? [] : this.adminTokens.list(),
|
|
11031
|
+
revoke: async (id) => this.adminTokens === null ? false : this.adminTokens.revoke(id)
|
|
11032
|
+
},
|
|
11033
|
+
audit: {
|
|
11034
|
+
record: async (event) => {
|
|
11035
|
+
await this.auditLog?.record(event);
|
|
11036
|
+
},
|
|
11037
|
+
list: async (input) => this.auditLog === null ? [] : this.auditLog.list(input)
|
|
11038
|
+
},
|
|
11039
|
+
setPassword: async (input) => {
|
|
11040
|
+
if (!this.userManager) throw new Error("User management not available");
|
|
11041
|
+
await this.userManager.resetPassword(input.userId, input.newPassword);
|
|
11042
|
+
await sessions().revokeAllForUser(input.userId, {
|
|
11043
|
+
...input.keepSessionId !== void 0 ? { exceptSessionId: input.keepSessionId } : {},
|
|
11044
|
+
reason: "password-changed"
|
|
11045
|
+
});
|
|
11046
|
+
},
|
|
11047
|
+
linkExternalIdentity: async (input) => {
|
|
11048
|
+
if (!this.externalIdentities || !this.userManager) throw new Error("External sign-in linking not available");
|
|
11049
|
+
if (!await this.userManager.findById(input.userId)) throw new Error(`User not found: ${input.userId}`);
|
|
11050
|
+
await this.externalIdentities.link(input);
|
|
11051
|
+
return { success: true };
|
|
11052
|
+
}
|
|
11053
|
+
};
|
|
11054
|
+
}
|
|
11055
|
+
/**
|
|
11056
|
+
* End every way `userId` could still get in: login sessions (and the
|
|
11057
|
+
* pre-session tokens, via the cutoff), scoped tokens, OAuth account links,
|
|
11058
|
+
* the TOTP row and the external sign-in links. Each step is attempted even
|
|
11059
|
+
* if an earlier one fails, and a failure is logged — a half-cleaned user is
|
|
11060
|
+
* worse than a loud one.
|
|
11061
|
+
*/
|
|
11062
|
+
async endAllAccess(userId) {
|
|
11063
|
+
const steps = [
|
|
11064
|
+
["sessions", async () => this.loginSessions?.revokeAllForUser(userId, { reason: "user-deleted" })],
|
|
11065
|
+
["scoped tokens", async () => {
|
|
11066
|
+
for (const token of await this.scopedTokenManager?.listForUser(userId) ?? []) await this.scopedTokenManager?.revoke(token.id);
|
|
11067
|
+
}],
|
|
11068
|
+
["oauth sessions", async () => {
|
|
11069
|
+
for (const session of await this.oauthSessionManager?.list() ?? []) if (session.userId === userId && session.revokedAt === null) await this.oauthSessionManager?.markRevoked(session.id);
|
|
11070
|
+
}],
|
|
11071
|
+
["admin tokens", async () => this.adminTokens?.revokeAllCreatedBy(userId)],
|
|
11072
|
+
["totp", async () => this.totpManager?.disable(userId)],
|
|
11073
|
+
["sign-in links", async () => this.externalIdentities?.removeAllForUser(userId)]
|
|
11074
|
+
];
|
|
11075
|
+
for (const [what, step] of steps) try {
|
|
11076
|
+
await step();
|
|
11077
|
+
} catch (err) {
|
|
11078
|
+
this.ctx.logger.error(`deleting a user: could not remove its ${what}`, { meta: {
|
|
11079
|
+
userId,
|
|
11080
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11081
|
+
} });
|
|
11082
|
+
}
|
|
11083
|
+
}
|
|
11084
|
+
/**
|
|
10356
11085
|
* Drop auth rows that are dead but were never removed: scoped tokens past
|
|
10357
11086
|
* their expiry, and OAuth sessions revoked longer ago than
|
|
10358
11087
|
* {@link OAUTH_REVOKED_RETENTION_MS}.
|
|
@@ -10366,9 +11095,13 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
10366
11095
|
try {
|
|
10367
11096
|
const tokens = await this.scopedTokenManager?.reapExpired() ?? 0;
|
|
10368
11097
|
const sessions = await this.oauthSessionManager?.reapRevoked(Date.now() - OAUTH_REVOKED_RETENTION_MS) ?? 0;
|
|
10369
|
-
|
|
11098
|
+
const logins = await this.loginSessions?.reap() ?? 0;
|
|
11099
|
+
await this.adminTokens?.reap();
|
|
11100
|
+
await this.auditLog?.reap();
|
|
11101
|
+
if (tokens > 0 || sessions > 0 || logins > 0) this.ctx.logger.info("auth retention swept", { meta: {
|
|
10370
11102
|
expiredTokens: tokens,
|
|
10371
|
-
revokedSessions: sessions
|
|
11103
|
+
revokedSessions: sessions,
|
|
11104
|
+
deadLoginSessions: logins
|
|
10372
11105
|
} });
|
|
10373
11106
|
} catch (err) {
|
|
10374
11107
|
this.ctx.logger.warn("auth retention sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -10387,28 +11120,29 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
10387
11120
|
this.scopedTokenManager = null;
|
|
10388
11121
|
this.oauthSessionManager = null;
|
|
10389
11122
|
this.externalIdentities = null;
|
|
11123
|
+
this.loginSessions = null;
|
|
11124
|
+
this.adminTokens = null;
|
|
11125
|
+
this.auditLog = null;
|
|
10390
11126
|
}
|
|
10391
11127
|
};
|
|
10392
11128
|
//#endregion
|
|
11129
|
+
exports.ADMIN_TOKEN_MAX_LIFETIME_MS = ADMIN_TOKEN_MAX_LIFETIME_MS;
|
|
11130
|
+
exports.ADMIN_TOKEN_PREFIX = ADMIN_TOKEN_PREFIX;
|
|
11131
|
+
exports.AdminTokenManager = AdminTokenManager;
|
|
10393
11132
|
exports.ApiKeyManager = ApiKeyManager;
|
|
11133
|
+
exports.AuthAuditLog = AuthAuditLog;
|
|
10394
11134
|
exports.AuthManager = AuthManager;
|
|
10395
11135
|
exports.FIRST_BOOT_ADMIN_PASSWORD = FIRST_BOOT_ADMIN_PASSWORD;
|
|
10396
11136
|
exports.FIRST_BOOT_ADMIN_USERNAME = FIRST_BOOT_ADMIN_USERNAME;
|
|
10397
11137
|
exports.LocalAuthAddon = LocalAuthAddon;
|
|
10398
11138
|
exports.default = LocalAuthAddon;
|
|
11139
|
+
exports.LoginSessionManager = LoginSessionManager;
|
|
11140
|
+
exports.REFRESH_TOKEN_PREFIX = REFRESH_TOKEN_PREFIX;
|
|
10399
11141
|
exports.ScopedTokenManager = ScopedTokenManager;
|
|
10400
11142
|
exports.SettingsReadUnavailableError = SettingsReadUnavailableError;
|
|
11143
|
+
exports.USER_MANAGEMENT_INTERNAL = USER_MANAGEMENT_INTERNAL;
|
|
10401
11144
|
exports.UserManager = UserManager;
|
|
11145
|
+
exports.attachUserManagementInternal = attachUserManagementInternal;
|
|
11146
|
+
exports.getUserManagementInternal = getUserManagementInternal;
|
|
10402
11147
|
exports.isSettingsReadUnavailable = isSettingsReadUnavailable;
|
|
10403
|
-
|
|
10404
|
-
enumerable: true,
|
|
10405
|
-
get: function() {
|
|
10406
|
-
return require_ms;
|
|
10407
|
-
}
|
|
10408
|
-
});
|
|
10409
|
-
Object.defineProperty(exports, "require_safe_buffer", {
|
|
10410
|
-
enumerable: true,
|
|
10411
|
-
get: function() {
|
|
10412
|
-
return require_safe_buffer;
|
|
10413
|
-
}
|
|
10414
|
-
});
|
|
11148
|
+
exports.parseTokenExpiry = parseTokenExpiry;
|