@camstack/system 1.2.427 → 1.2.429
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/agent-https-names-CpUSuP4w.js +120 -0
- package/dist/agent-https-names-uetr4OQl.mjs +91 -0
- package/dist/auth/auth-audit-log.d.ts +1 -1
- package/dist/auth/device-ca-store.d.ts +57 -0
- package/dist/auth/device-ca.d.ts +75 -0
- package/dist/auth/device-credential-manager.d.ts +154 -0
- package/dist/auth/login-session-manager.d.ts +14 -2
- package/dist/auth/user-management-internal.d.ts +10 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- 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 +10 -6
- package/dist/builtins/keychain/index.mjs +8 -4
- 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 +6 -0
- package/dist/builtins/local-auth/local-auth.addon.js +955 -47
- package/dist/builtins/local-auth/local-auth.addon.mjs +946 -41
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- 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/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +3 -3
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.d.ts +18 -0
- package/dist/builtins/system-config/system-config.addon.js +834 -18
- package/dist/builtins/system-config/system-config.addon.mjs +819 -19
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-DvCGZEUI.js → composition-sources-C2VNX6tF.js} +1 -1
- package/dist/{composition-sources-BQNxXQZG.mjs → composition-sources-SYtYuAgh.mjs} +1 -1
- package/dist/{constants-CR4If_lP.mjs → constants-CKuWoWvc.mjs} +1 -1
- package/dist/{constants-CCSJGnjs.js → constants-RI-hGP5S.js} +1 -1
- package/dist/device-ca-BWfdDB3k.js +462 -0
- package/dist/device-ca-R7DKKZL6.mjs +355 -0
- package/dist/{dist-BQG3QFg7.js → dist-Bgy_AmwX.js} +554 -0
- package/dist/{dist-DHSJV4Qc.mjs → dist-CSSfTM18.mjs} +507 -1
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +59 -12
- package/dist/index.mjs +10 -9
- package/dist/kernel/index.d.ts +2 -2
- package/dist/kernel/moleculer/mesh-mtls/index.d.ts +4 -1
- package/dist/kernel/moleculer/mesh-mtls/mesh-certificate-registry.d.ts +91 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-enroll.d.ts +4 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-peer-transport.d.ts +23 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-renewal.d.ts +126 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-tls-gate.d.ts +40 -0
- package/dist/kernel/moleculer/mesh-mtls/tls-tcp-transporter.d.ts +15 -0
- package/dist/{restore-archive-DH1Pxq_1.mjs → restore-archive-AIwMHO5_.mjs} +1 -1
- package/dist/{restore-archive-Dccm8lkA.js → restore-archive-BYUEMsog.js} +1 -1
- package/dist/{retired-settings-keys-BfGaSeQI.js → retired-settings-keys-BTPkRLTS.js} +1 -1
- package/dist/{retired-settings-keys-CPz7zpza.mjs → retired-settings-keys-H0rYXT27.mjs} +1 -1
- package/dist/tls/agent-https-names.d.ts +27 -0
- package/dist/tls/agent-https.d.ts +111 -0
- package/dist/tls/cert-issue.d.ts +7 -0
- package/dist/tls/index.d.ts +3 -0
- package/dist/{tls-BgImdxCO.js → tls-C5IYg3V5.js} +406 -0
- package/dist/{tls-CaOLZ09W.mjs → tls-CpcGkeJV.mjs} +314 -4
- package/dist/{wal-maintenance-DItkuOvu.js → wal-maintenance-BSGsxKW7.js} +0 -0
- package/dist/{wal-maintenance-Drqhcjit.mjs → wal-maintenance-DW0FfGV7.mjs} +0 -0
- package/package.json +4 -1
- package/dist/jwt-signing-key-BZm055IP.js +0 -174
- package/dist/jwt-signing-key-DL-qk1SM.mjs +0 -127
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Et as deviceCredentialsCapability, H as ScopedTokenSchema, Jn as userManagementCapability, Q as authProviderCapability, S as DEVICE_NONCE_TTL_MS, Vt as formatDeviceRevoker, _r as BaseAddon, it as buildDeviceSignatureMessage, q as UserRecordSchema, r as ApiKeyRecordSchema, rt as buildDeviceEnrollmentLink, x as DEVICE_ENROLLMENT_TTL_MS } from "../../dist-CSSfTM18.mjs";
|
|
3
3
|
import { t as require_safe_buffer } from "../../safe-buffer-B1OjL1I9.mjs";
|
|
4
4
|
import { t as require_ms } from "../../ms-CgH2OFlD.mjs";
|
|
5
|
-
import { a as loadJwtSigningKey, i as getProcessJwtSigningKeyState, o as
|
|
5
|
+
import { _ as setProcessJwtSigningKey, a as importDevicePublicKey, c as rebuildDeviceCaCertificate, d as JWT_SIGNING_KEY_REF, g as processJwtSigningKey, h as loadJwtSigningKey, i as generateDeviceCa, l as verifyDeviceLeaf, m as getProcessJwtSigningKeyState, n as DEVICE_CA_KEY_REF, o as issueDeviceLeaf, r as DEVICE_CA_SETTINGS_SECTION, s as loadDeviceCa, t as DEVICE_CA_FIELD, u as verifyDeviceSignature } from "../../device-ca-R7DKKZL6.mjs";
|
|
6
6
|
import * as crypto$1 from "node:crypto";
|
|
7
|
-
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
7
|
+
import { X509Certificate, createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
8
8
|
import nodeCrypto from "crypto";
|
|
9
9
|
import * as path$1 from "node:path";
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
//#region src/auth/parse-record.ts
|
|
12
|
-
function parseRecord$
|
|
12
|
+
function parseRecord$2(kind, schema, data) {
|
|
13
13
|
try {
|
|
14
14
|
return schema.parse(data);
|
|
15
15
|
} catch (err) {
|
|
@@ -31,7 +31,7 @@ function parseRecord$1(kind, schema, data) {
|
|
|
31
31
|
*/
|
|
32
32
|
var API_KEYS_COLLECTION$1 = "api_keys";
|
|
33
33
|
function parseApiKey(data) {
|
|
34
|
-
return parseRecord$
|
|
34
|
+
return parseRecord$2("api-key", ApiKeyRecordSchema, data);
|
|
35
35
|
}
|
|
36
36
|
var ApiKeyManager = class {
|
|
37
37
|
storageAccess;
|
|
@@ -153,7 +153,7 @@ function genSalt(rounds, seed_length, callback) {
|
|
|
153
153
|
* @returns {!Promise} If `callback` has been omitted
|
|
154
154
|
* @throws {Error} If `callback` is present but not a function
|
|
155
155
|
*/
|
|
156
|
-
function hash$
|
|
156
|
+
function hash$2(password, salt, callback, progressCallback) {
|
|
157
157
|
function _async(callback) {
|
|
158
158
|
if (typeof password === "string" && typeof salt === "number") genSalt(salt, function(err, salt) {
|
|
159
159
|
_hash(password, salt, callback, progressCallback);
|
|
@@ -206,7 +206,7 @@ function compare(password, hashValue, callback, progressCallback) {
|
|
|
206
206
|
nextTick(callback.bind(this, null, false));
|
|
207
207
|
return;
|
|
208
208
|
}
|
|
209
|
-
hash$
|
|
209
|
+
hash$2(password, hashValue.substring(0, 29), function(err, comp) {
|
|
210
210
|
if (err) callback(err);
|
|
211
211
|
else callback(null, safeStringCompare(comp, hashValue));
|
|
212
212
|
}, progressCallback);
|
|
@@ -6077,7 +6077,7 @@ var AuthManager = class {
|
|
|
6077
6077
|
return import_jsonwebtoken.verify(token, this.jwtSecret);
|
|
6078
6078
|
}
|
|
6079
6079
|
async hashPassword(password) {
|
|
6080
|
-
return hash$
|
|
6080
|
+
return hash$2(password, 10);
|
|
6081
6081
|
}
|
|
6082
6082
|
async comparePassword(password, hash) {
|
|
6083
6083
|
return compare(password, hash);
|
|
@@ -6272,7 +6272,7 @@ var AuthManager = class {
|
|
|
6272
6272
|
var TOKENS_COLLECTION = "scoped_tokens";
|
|
6273
6273
|
var TOKEN_PREFIX = "cst_";
|
|
6274
6274
|
function parseToken(data) {
|
|
6275
|
-
return parseRecord$
|
|
6275
|
+
return parseRecord$2("scoped-token", ScopedTokenSchema, data);
|
|
6276
6276
|
}
|
|
6277
6277
|
/**
|
|
6278
6278
|
* Manages scoped API tokens with restricted addon/route/capability access.
|
|
@@ -6555,6 +6555,10 @@ var LOGIN_SESSIONS_COLUMNS = [
|
|
|
6555
6555
|
{
|
|
6556
6556
|
name: "ip",
|
|
6557
6557
|
type: "TEXT"
|
|
6558
|
+
},
|
|
6559
|
+
{
|
|
6560
|
+
name: "credentialId",
|
|
6561
|
+
type: "TEXT"
|
|
6558
6562
|
}
|
|
6559
6563
|
];
|
|
6560
6564
|
var LOGIN_SESSION_CUTOFFS_COLUMNS = [{
|
|
@@ -6572,7 +6576,8 @@ var METHODS = [
|
|
|
6572
6576
|
"passkey",
|
|
6573
6577
|
"sso",
|
|
6574
6578
|
"handoff",
|
|
6575
|
-
"exchange"
|
|
6579
|
+
"exchange",
|
|
6580
|
+
"device-certificate"
|
|
6576
6581
|
];
|
|
6577
6582
|
var REFRESH_TOKEN_PREFIX = "csr_";
|
|
6578
6583
|
/**
|
|
@@ -6588,20 +6593,20 @@ var MAX_CLIENT_LABEL = 200;
|
|
|
6588
6593
|
function hashToken(token) {
|
|
6589
6594
|
return crypto$1.createHash("sha256").update(token).digest("hex");
|
|
6590
6595
|
}
|
|
6591
|
-
function str(data, key) {
|
|
6596
|
+
function str$1(data, key) {
|
|
6592
6597
|
const value = data[key];
|
|
6593
6598
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
6594
6599
|
}
|
|
6595
|
-
function num(data, key) {
|
|
6600
|
+
function num$1(data, key) {
|
|
6596
6601
|
const value = data[key];
|
|
6597
6602
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
6598
6603
|
}
|
|
6599
6604
|
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 id = str$1(data, "id");
|
|
6606
|
+
const userId = str$1(data, "userId");
|
|
6607
|
+
const createdAt = num$1(data, "createdAt");
|
|
6608
|
+
const lastUsedAt = num$1(data, "lastUsedAt");
|
|
6609
|
+
const expiresAt = num$1(data, "expiresAt");
|
|
6605
6610
|
const method = METHODS.find((m) => m === data["method"]);
|
|
6606
6611
|
if (id === null || userId === null || createdAt === null || expiresAt === null) return null;
|
|
6607
6612
|
return {
|
|
@@ -6612,13 +6617,14 @@ function parseSession$1(data) {
|
|
|
6612
6617
|
createdAt,
|
|
6613
6618
|
lastUsedAt: lastUsedAt ?? createdAt,
|
|
6614
6619
|
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")
|
|
6620
|
+
revokedAt: num$1(data, "revokedAt"),
|
|
6621
|
+
revokedReason: str$1(data, "revokedReason"),
|
|
6622
|
+
refreshHash: str$1(data, "refreshHash"),
|
|
6623
|
+
previousRefreshHash: str$1(data, "previousRefreshHash"),
|
|
6624
|
+
rotatedAt: num$1(data, "rotatedAt"),
|
|
6625
|
+
client: str$1(data, "client"),
|
|
6626
|
+
ip: str$1(data, "ip"),
|
|
6627
|
+
credentialId: str$1(data, "credentialId")
|
|
6622
6628
|
};
|
|
6623
6629
|
}
|
|
6624
6630
|
function toLoginSessionSummary(record) {
|
|
@@ -6647,8 +6653,8 @@ var LoginSessionManager = class {
|
|
|
6647
6653
|
}
|
|
6648
6654
|
const cutoffs = await this.store.query.query({ collection: LOGIN_SESSION_CUTOFFS_COLLECTION });
|
|
6649
6655
|
for (const row of cutoffs) {
|
|
6650
|
-
const userId = str(row.data, "userId");
|
|
6651
|
-
const notBefore = num(row.data, "notBefore");
|
|
6656
|
+
const userId = str$1(row.data, "userId");
|
|
6657
|
+
const notBefore = num$1(row.data, "notBefore");
|
|
6652
6658
|
if (userId !== null && notBefore !== null) this.cutoffs.set(userId, notBefore);
|
|
6653
6659
|
}
|
|
6654
6660
|
}
|
|
@@ -6669,7 +6675,8 @@ var LoginSessionManager = class {
|
|
|
6669
6675
|
previousRefreshHash: null,
|
|
6670
6676
|
rotatedAt: null,
|
|
6671
6677
|
client: input.client === void 0 ? null : input.client.slice(0, MAX_CLIENT_LABEL),
|
|
6672
|
-
ip: input.ip ?? null
|
|
6678
|
+
ip: input.ip ?? null,
|
|
6679
|
+
credentialId: input.credentialId ?? null
|
|
6673
6680
|
};
|
|
6674
6681
|
await this.store.insert.mutate({
|
|
6675
6682
|
collection: LOGIN_SESSIONS_COLLECTION,
|
|
@@ -6699,6 +6706,16 @@ var LoginSessionManager = class {
|
|
|
6699
6706
|
legacyCutoff(userId) {
|
|
6700
6707
|
return this.cutoffs.get(userId) ?? 0;
|
|
6701
6708
|
}
|
|
6709
|
+
/**
|
|
6710
|
+
* The session a refresh token belongs to, WITHOUT rotating it - so the
|
|
6711
|
+
* refresh path can judge the session (its device credential) before it
|
|
6712
|
+
* spends the token. `null` when the token names no session.
|
|
6713
|
+
*/
|
|
6714
|
+
peekByRefreshToken(refreshToken) {
|
|
6715
|
+
if (!refreshToken.startsWith("csr_")) return null;
|
|
6716
|
+
const sessionId = this.byRefreshHash.get(hashToken(refreshToken));
|
|
6717
|
+
return sessionId === void 0 ? null : this.sessions.get(sessionId) ?? null;
|
|
6718
|
+
}
|
|
6702
6719
|
/** Swap a refresh token for a rotated one, sliding the session's expiry to now + `lifetimeMs`. */
|
|
6703
6720
|
async refresh(refreshToken, lifetimeMs) {
|
|
6704
6721
|
if (!refreshToken.startsWith("csr_")) return {
|
|
@@ -6802,6 +6819,23 @@ var LoginSessionManager = class {
|
|
|
6802
6819
|
for (const session of ended) await this.persist(session);
|
|
6803
6820
|
return ended.length;
|
|
6804
6821
|
}
|
|
6822
|
+
/** End every live session opened by device credential `credentialId`. Returns how many. */
|
|
6823
|
+
async revokeByCredential(credentialId, reason = "credential-revoked") {
|
|
6824
|
+
const at = this.now();
|
|
6825
|
+
const ended = [];
|
|
6826
|
+
for (const session of this.sessions.values()) {
|
|
6827
|
+
if (session.credentialId !== credentialId || session.revokedAt !== null) continue;
|
|
6828
|
+
const updated = {
|
|
6829
|
+
...session,
|
|
6830
|
+
revokedAt: at,
|
|
6831
|
+
revokedReason: reason
|
|
6832
|
+
};
|
|
6833
|
+
this.replace(session, updated);
|
|
6834
|
+
ended.push(updated);
|
|
6835
|
+
}
|
|
6836
|
+
for (const session of ended) await this.persist(session);
|
|
6837
|
+
return ended.length;
|
|
6838
|
+
}
|
|
6805
6839
|
/** `userId`'s live sessions, newest first. */
|
|
6806
6840
|
listForUser(userId) {
|
|
6807
6841
|
return this.listActive().filter((s) => s.userId === userId);
|
|
@@ -6955,7 +6989,7 @@ var ADMIN_TOKEN_PREFIX = "csa_";
|
|
|
6955
6989
|
var ADMIN_TOKEN_MAX_LIFETIME_MS = 90 * 864e5;
|
|
6956
6990
|
var DEAD_RETENTION_MS = 30 * 864e5;
|
|
6957
6991
|
var TOUCH_EVERY_MS = 6e4;
|
|
6958
|
-
function hash(token) {
|
|
6992
|
+
function hash$1(token) {
|
|
6959
6993
|
return crypto$1.createHash("sha256").update(token).digest("hex");
|
|
6960
6994
|
}
|
|
6961
6995
|
function parse(data) {
|
|
@@ -6995,7 +7029,7 @@ var AdminTokenManager = class {
|
|
|
6995
7029
|
id: crypto$1.randomUUID(),
|
|
6996
7030
|
createdBy: input.createdBy,
|
|
6997
7031
|
name,
|
|
6998
|
-
tokenHash: hash(token),
|
|
7032
|
+
tokenHash: hash$1(token),
|
|
6999
7033
|
tokenPrefix: token.slice(0, 12),
|
|
7000
7034
|
createdAt: at,
|
|
7001
7035
|
expiresAt: input.expiresAt,
|
|
@@ -7019,7 +7053,7 @@ var AdminTokenManager = class {
|
|
|
7019
7053
|
if (!token.startsWith("csa_")) return null;
|
|
7020
7054
|
const first = (await this.store.query.query({
|
|
7021
7055
|
collection: ADMIN_TOKENS_COLLECTION,
|
|
7022
|
-
filter: { where: { tokenHash: hash(token) } }
|
|
7056
|
+
filter: { where: { tokenHash: hash$1(token) } }
|
|
7023
7057
|
}))[0];
|
|
7024
7058
|
const stored = first === void 0 ? null : parse(first.data);
|
|
7025
7059
|
if (stored === null || stored.revokedAt !== null) return null;
|
|
@@ -7080,6 +7114,678 @@ var AdminTokenManager = class {
|
|
|
7080
7114
|
}
|
|
7081
7115
|
};
|
|
7082
7116
|
//#endregion
|
|
7117
|
+
//#region src/auth/device-ca-store.ts
|
|
7118
|
+
function hasCertificate(value) {
|
|
7119
|
+
return typeof value === "string" && value.includes("BEGIN CERTIFICATE");
|
|
7120
|
+
}
|
|
7121
|
+
var DeviceCaStore = class {
|
|
7122
|
+
deps;
|
|
7123
|
+
loaded = null;
|
|
7124
|
+
creating = null;
|
|
7125
|
+
lastReported = null;
|
|
7126
|
+
constructor(deps) {
|
|
7127
|
+
this.deps = deps;
|
|
7128
|
+
}
|
|
7129
|
+
/** The CA when one exists (completing an interrupted creation). Never creates a NEW one. */
|
|
7130
|
+
async lookup() {
|
|
7131
|
+
if (this.loaded !== null) return {
|
|
7132
|
+
state: "ready",
|
|
7133
|
+
ca: this.loaded
|
|
7134
|
+
};
|
|
7135
|
+
const secrets = this.deps.secrets;
|
|
7136
|
+
if (secrets === null) return {
|
|
7137
|
+
state: "unavailable",
|
|
7138
|
+
reason: "no keychain door in this process"
|
|
7139
|
+
};
|
|
7140
|
+
let entry;
|
|
7141
|
+
try {
|
|
7142
|
+
entry = (await secrets.resolve([DEVICE_CA_KEY_REF]))[DEVICE_CA_KEY_REF];
|
|
7143
|
+
} catch (err) {
|
|
7144
|
+
return {
|
|
7145
|
+
state: "unavailable",
|
|
7146
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
7147
|
+
};
|
|
7148
|
+
}
|
|
7149
|
+
if (entry === void 0) return {
|
|
7150
|
+
state: "unavailable",
|
|
7151
|
+
reason: "no-answer"
|
|
7152
|
+
};
|
|
7153
|
+
if (entry.kind === "unavailable") return {
|
|
7154
|
+
state: "unavailable",
|
|
7155
|
+
reason: entry.reason
|
|
7156
|
+
};
|
|
7157
|
+
if (entry.kind === "forbidden") return {
|
|
7158
|
+
state: "unavailable",
|
|
7159
|
+
reason: "forbidden"
|
|
7160
|
+
};
|
|
7161
|
+
let section;
|
|
7162
|
+
try {
|
|
7163
|
+
section = await this.deps.readSection(DEVICE_CA_SETTINGS_SECTION);
|
|
7164
|
+
} catch (err) {
|
|
7165
|
+
return {
|
|
7166
|
+
state: "unavailable",
|
|
7167
|
+
reason: `settings unreadable: ${err instanceof Error ? err.message : String(err)}`
|
|
7168
|
+
};
|
|
7169
|
+
}
|
|
7170
|
+
const certPem = section[DEVICE_CA_FIELD];
|
|
7171
|
+
if (entry.kind === "missing") {
|
|
7172
|
+
if (!hasCertificate(certPem)) return { state: "absent" };
|
|
7173
|
+
return this.report("the device CA key is missing from the keychain but cluster.deviceCa is present - not creating a new CA (it would orphan every enrolled device); restore the key (secret://system/global/deviceCaKey) or remove cluster.deviceCa knowingly");
|
|
7174
|
+
}
|
|
7175
|
+
try {
|
|
7176
|
+
if (!hasCertificate(certPem)) {
|
|
7177
|
+
const rebuilt = await rebuildDeviceCaCertificate(entry.value);
|
|
7178
|
+
await this.deps.writeSection(DEVICE_CA_SETTINGS_SECTION, { [DEVICE_CA_FIELD]: rebuilt.certPem });
|
|
7179
|
+
this.deps.logger?.warn("Device CA certificate rebuilt from the stored key", { meta: { reason: "cluster.deviceCa was missing next to the key" } });
|
|
7180
|
+
this.loaded = rebuilt;
|
|
7181
|
+
return {
|
|
7182
|
+
state: "ready",
|
|
7183
|
+
ca: rebuilt
|
|
7184
|
+
};
|
|
7185
|
+
}
|
|
7186
|
+
this.loaded = await loadDeviceCa(certPem, entry.value);
|
|
7187
|
+
return {
|
|
7188
|
+
state: "ready",
|
|
7189
|
+
ca: this.loaded
|
|
7190
|
+
};
|
|
7191
|
+
} catch (err) {
|
|
7192
|
+
return {
|
|
7193
|
+
state: "unavailable",
|
|
7194
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
7195
|
+
};
|
|
7196
|
+
}
|
|
7197
|
+
}
|
|
7198
|
+
/** The CA, created now if neither half exists. Throws on any inconsistent or unreadable state. */
|
|
7199
|
+
async ensure() {
|
|
7200
|
+
const found = await this.lookup();
|
|
7201
|
+
if (found.state === "ready") return found.ca;
|
|
7202
|
+
if (found.state === "unavailable") throw new Error(`device CA unavailable: ${found.reason}`);
|
|
7203
|
+
if (this.creating === null) this.creating = this.create().finally(() => {
|
|
7204
|
+
this.creating = null;
|
|
7205
|
+
});
|
|
7206
|
+
return this.creating;
|
|
7207
|
+
}
|
|
7208
|
+
report(reason) {
|
|
7209
|
+
if (this.lastReported !== reason) {
|
|
7210
|
+
this.lastReported = reason;
|
|
7211
|
+
this.deps.logger?.error(`Device CA is inconsistent: ${reason}`);
|
|
7212
|
+
}
|
|
7213
|
+
return {
|
|
7214
|
+
state: "unavailable",
|
|
7215
|
+
reason
|
|
7216
|
+
};
|
|
7217
|
+
}
|
|
7218
|
+
async create() {
|
|
7219
|
+
const secrets = this.deps.secrets;
|
|
7220
|
+
if (secrets === null) throw new Error("device CA unavailable: no keychain door");
|
|
7221
|
+
const generated = await generateDeviceCa();
|
|
7222
|
+
await secrets.put(DEVICE_CA_KEY_REF, generated.keyPem);
|
|
7223
|
+
await this.deps.writeSection(DEVICE_CA_SETTINGS_SECTION, { [DEVICE_CA_FIELD]: generated.certPem });
|
|
7224
|
+
this.loaded = await loadDeviceCa(generated.certPem, generated.keyPem);
|
|
7225
|
+
return this.loaded;
|
|
7226
|
+
}
|
|
7227
|
+
};
|
|
7228
|
+
//#endregion
|
|
7229
|
+
//#region src/auth/device-credential-manager.ts
|
|
7230
|
+
/**
|
|
7231
|
+
* Device credentials (D855): the registry of enrolled devices, the one-shot
|
|
7232
|
+
* enrollment codes, the login nonces and the certificate checks.
|
|
7233
|
+
*
|
|
7234
|
+
* Hub-owned and persisted in `device_credentials` (the settings store);
|
|
7235
|
+
* agents persist nothing. Enrollment codes and nonces live in memory only —
|
|
7236
|
+
* both die in minutes and a hub restart simply asks the user to start again.
|
|
7237
|
+
*
|
|
7238
|
+
* Every refusal is logged with its reason and the credential/user it concerns
|
|
7239
|
+
* (`tags: { credentialId | userId }`) — never the code, the nonce or a
|
|
7240
|
+
* signature.
|
|
7241
|
+
*/
|
|
7242
|
+
/**
|
|
7243
|
+
* @durable class=config owner=local-auth
|
|
7244
|
+
* write="an enrollment completes (QR or self) inserts a row; a renewal rewrites
|
|
7245
|
+
* fingerprint/expiry; every accepted certificate login rewrites lastUsedAt/From;
|
|
7246
|
+
* an admin revokes or renames"
|
|
7247
|
+
* retention="a revoked or long-expired row is kept 90 days for the audit question
|
|
7248
|
+
* 'which device was that', then the daily reap deletes it; an active row is
|
|
7249
|
+
* never deleted"
|
|
7250
|
+
*/
|
|
7251
|
+
var DEVICE_CREDENTIALS_COLLECTION = "device_credentials";
|
|
7252
|
+
var DEVICE_CREDENTIALS_COLUMNS = [
|
|
7253
|
+
{
|
|
7254
|
+
name: "id",
|
|
7255
|
+
type: "TEXT",
|
|
7256
|
+
primaryKey: true,
|
|
7257
|
+
notNull: true
|
|
7258
|
+
},
|
|
7259
|
+
{
|
|
7260
|
+
name: "userId",
|
|
7261
|
+
type: "TEXT",
|
|
7262
|
+
notNull: true
|
|
7263
|
+
},
|
|
7264
|
+
{
|
|
7265
|
+
name: "deviceName",
|
|
7266
|
+
type: "TEXT",
|
|
7267
|
+
notNull: true
|
|
7268
|
+
},
|
|
7269
|
+
{
|
|
7270
|
+
name: "platform",
|
|
7271
|
+
type: "TEXT",
|
|
7272
|
+
notNull: true
|
|
7273
|
+
},
|
|
7274
|
+
{
|
|
7275
|
+
name: "enrolledVia",
|
|
7276
|
+
type: "TEXT",
|
|
7277
|
+
notNull: true
|
|
7278
|
+
},
|
|
7279
|
+
{
|
|
7280
|
+
name: "fingerprint",
|
|
7281
|
+
type: "TEXT",
|
|
7282
|
+
notNull: true
|
|
7283
|
+
},
|
|
7284
|
+
{
|
|
7285
|
+
name: "serial",
|
|
7286
|
+
type: "TEXT",
|
|
7287
|
+
notNull: true
|
|
7288
|
+
},
|
|
7289
|
+
{
|
|
7290
|
+
name: "previousFingerprint",
|
|
7291
|
+
type: "TEXT"
|
|
7292
|
+
},
|
|
7293
|
+
{
|
|
7294
|
+
name: "renewedAt",
|
|
7295
|
+
type: "INTEGER"
|
|
7296
|
+
},
|
|
7297
|
+
{
|
|
7298
|
+
name: "publicKey",
|
|
7299
|
+
type: "TEXT",
|
|
7300
|
+
notNull: true
|
|
7301
|
+
},
|
|
7302
|
+
{
|
|
7303
|
+
name: "issuedAt",
|
|
7304
|
+
type: "INTEGER",
|
|
7305
|
+
notNull: true
|
|
7306
|
+
},
|
|
7307
|
+
{
|
|
7308
|
+
name: "expiresAt",
|
|
7309
|
+
type: "INTEGER",
|
|
7310
|
+
notNull: true
|
|
7311
|
+
},
|
|
7312
|
+
{
|
|
7313
|
+
name: "lastUsedAt",
|
|
7314
|
+
type: "INTEGER"
|
|
7315
|
+
},
|
|
7316
|
+
{
|
|
7317
|
+
name: "lastUsedFrom",
|
|
7318
|
+
type: "TEXT"
|
|
7319
|
+
},
|
|
7320
|
+
{
|
|
7321
|
+
name: "revokedAt",
|
|
7322
|
+
type: "INTEGER"
|
|
7323
|
+
},
|
|
7324
|
+
{
|
|
7325
|
+
name: "revokedBy",
|
|
7326
|
+
type: "TEXT"
|
|
7327
|
+
}
|
|
7328
|
+
];
|
|
7329
|
+
var MAX_PENDING = 2e3;
|
|
7330
|
+
/**
|
|
7331
|
+
* Outstanding login nonces are bounded PER CLIENT (a partition = the client
|
|
7332
|
+
* address): one flooder evicts only its own oldest nonces, never another
|
|
7333
|
+
* client's. {@link MAX_NONCES_TOTAL} is a memory ceiling that only a flood from
|
|
7334
|
+
* hundreds of distinct addresses can reach.
|
|
7335
|
+
*/
|
|
7336
|
+
var MAX_NONCES_PER_PARTITION = 50;
|
|
7337
|
+
var MAX_NONCES_TOTAL = 2e4;
|
|
7338
|
+
/**
|
|
7339
|
+
* After a renewal the PREVIOUS leaf still logs in for this long, so a device
|
|
7340
|
+
* that crashed between the hub's answer and its own write is not locked out;
|
|
7341
|
+
* after it, the old leaf is `superseded` (not definitive: the app holds the
|
|
7342
|
+
* newest certificate, so seeing it means the device lost the new one).
|
|
7343
|
+
*/
|
|
7344
|
+
var PREVIOUS_LEAF_GRACE_MS = 24 * 36e5;
|
|
7345
|
+
var REAP_RETENTION_MS = 90 * 864e5;
|
|
7346
|
+
function hash(value) {
|
|
7347
|
+
return crypto$1.createHash("sha256").update(value).digest("hex");
|
|
7348
|
+
}
|
|
7349
|
+
function str(data, key) {
|
|
7350
|
+
const v = data[key];
|
|
7351
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
7352
|
+
}
|
|
7353
|
+
function num(data, key) {
|
|
7354
|
+
const v = data[key];
|
|
7355
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
7356
|
+
}
|
|
7357
|
+
var PLATFORMS = [
|
|
7358
|
+
"ios",
|
|
7359
|
+
"android",
|
|
7360
|
+
"web",
|
|
7361
|
+
"other"
|
|
7362
|
+
];
|
|
7363
|
+
function parseRecord$1(data) {
|
|
7364
|
+
const id = str(data, "id");
|
|
7365
|
+
const userId = str(data, "userId");
|
|
7366
|
+
const deviceName = str(data, "deviceName");
|
|
7367
|
+
const fingerprint = str(data, "fingerprint");
|
|
7368
|
+
const serial = str(data, "serial");
|
|
7369
|
+
const publicKey = str(data, "publicKey");
|
|
7370
|
+
const issuedAt = num(data, "issuedAt");
|
|
7371
|
+
const expiresAt = num(data, "expiresAt");
|
|
7372
|
+
if (id === null || userId === null || deviceName === null || fingerprint === null || serial === null || publicKey === null || issuedAt === null || expiresAt === null) return null;
|
|
7373
|
+
return {
|
|
7374
|
+
id,
|
|
7375
|
+
userId,
|
|
7376
|
+
deviceName,
|
|
7377
|
+
platform: PLATFORMS.find((p) => p === data["platform"]) ?? "other",
|
|
7378
|
+
enrolledVia: data["enrolledVia"] === "self" ? "self" : "qr",
|
|
7379
|
+
fingerprint,
|
|
7380
|
+
serial,
|
|
7381
|
+
previousFingerprint: str(data, "previousFingerprint"),
|
|
7382
|
+
renewedAt: num(data, "renewedAt"),
|
|
7383
|
+
publicKey,
|
|
7384
|
+
issuedAt,
|
|
7385
|
+
expiresAt,
|
|
7386
|
+
lastUsedAt: num(data, "lastUsedAt"),
|
|
7387
|
+
lastUsedFrom: str(data, "lastUsedFrom"),
|
|
7388
|
+
revokedAt: num(data, "revokedAt"),
|
|
7389
|
+
revokedBy: str(data, "revokedBy")
|
|
7390
|
+
};
|
|
7391
|
+
}
|
|
7392
|
+
var DeviceCredentialManager = class {
|
|
7393
|
+
store;
|
|
7394
|
+
deps;
|
|
7395
|
+
records = /* @__PURE__ */ new Map();
|
|
7396
|
+
pendingEnrollments = /* @__PURE__ */ new Map();
|
|
7397
|
+
usedCodes = /* @__PURE__ */ new Map();
|
|
7398
|
+
nonces = /* @__PURE__ */ new Map();
|
|
7399
|
+
now;
|
|
7400
|
+
constructor(store, deps) {
|
|
7401
|
+
this.store = store;
|
|
7402
|
+
this.deps = deps;
|
|
7403
|
+
this.now = deps.now ?? Date.now;
|
|
7404
|
+
}
|
|
7405
|
+
async load() {
|
|
7406
|
+
const rows = await this.store.query.query({ collection: DEVICE_CREDENTIALS_COLLECTION });
|
|
7407
|
+
for (const row of rows) {
|
|
7408
|
+
const record = parseRecord$1(row.data);
|
|
7409
|
+
if (record !== null) this.records.set(record.id, record);
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
/** A one-shot code for `userId` (admin path). Returned once; only its hash is kept. */
|
|
7413
|
+
startEnrollment(input) {
|
|
7414
|
+
this.prune();
|
|
7415
|
+
if (this.pendingEnrollments.size >= MAX_PENDING) throw new Error("Too many pending device enrollments — try again shortly");
|
|
7416
|
+
const code = crypto$1.randomBytes(24).toString("base64url");
|
|
7417
|
+
const expiresAt = this.now() + DEVICE_ENROLLMENT_TTL_MS;
|
|
7418
|
+
this.pendingEnrollments.set(hash(code), {
|
|
7419
|
+
userId: input.userId,
|
|
7420
|
+
deviceName: input.deviceName,
|
|
7421
|
+
createdBy: input.createdBy,
|
|
7422
|
+
expiresAt
|
|
7423
|
+
});
|
|
7424
|
+
return {
|
|
7425
|
+
code,
|
|
7426
|
+
expiresAt
|
|
7427
|
+
};
|
|
7428
|
+
}
|
|
7429
|
+
/** Redeem a code with the device's public key (public procedure). */
|
|
7430
|
+
async completeEnrollment(input) {
|
|
7431
|
+
const key = hash(input.code);
|
|
7432
|
+
const pending = this.pendingEnrollments.get(key);
|
|
7433
|
+
if (pending === void 0) {
|
|
7434
|
+
const wasUsed = this.usedCodes.has(key);
|
|
7435
|
+
return this.refuse("enroll", "unknown-or-used-code", { detail: wasUsed ? "code already redeemed" : "code unknown" });
|
|
7436
|
+
}
|
|
7437
|
+
this.pendingEnrollments.delete(key);
|
|
7438
|
+
this.usedCodes.set(key, pending.expiresAt);
|
|
7439
|
+
if (this.now() > pending.expiresAt) return this.refuse("enroll", "expired-code", { userId: pending.userId });
|
|
7440
|
+
return this.issueNew({
|
|
7441
|
+
userId: pending.userId,
|
|
7442
|
+
deviceName: input.deviceName ?? pending.deviceName,
|
|
7443
|
+
platform: input.platform,
|
|
7444
|
+
publicKeySpki: input.publicKeySpki,
|
|
7445
|
+
enrolledVia: "qr"
|
|
7446
|
+
});
|
|
7447
|
+
}
|
|
7448
|
+
/** A signed-in user's own device (the caller has already proved a step-up). */
|
|
7449
|
+
async enrollSelf(input) {
|
|
7450
|
+
if (!await this.deps.selfEnrollmentAllowed()) return this.refuse("enroll-self", "self-enrollment-disabled", { userId: input.userId });
|
|
7451
|
+
return this.issueNew({
|
|
7452
|
+
...input,
|
|
7453
|
+
enrolledVia: "self"
|
|
7454
|
+
});
|
|
7455
|
+
}
|
|
7456
|
+
async issueNew(input) {
|
|
7457
|
+
const user = await this.deps.findUser(input.userId);
|
|
7458
|
+
if (user === null) return this.refuse("enroll", "user-gone", { userId: input.userId });
|
|
7459
|
+
const spki = Buffer.from(input.publicKeySpki, "base64url");
|
|
7460
|
+
try {
|
|
7461
|
+
await importDevicePublicKey(spki);
|
|
7462
|
+
} catch {
|
|
7463
|
+
return this.refuse("enroll", "bad-public-key", { userId: input.userId });
|
|
7464
|
+
}
|
|
7465
|
+
let ca;
|
|
7466
|
+
try {
|
|
7467
|
+
ca = await this.deps.ca.ensure();
|
|
7468
|
+
} catch (error) {
|
|
7469
|
+
this.deps.logger?.error("Device enrollment refused: the device CA is unavailable", {
|
|
7470
|
+
tags: { userId: input.userId },
|
|
7471
|
+
meta: { error: error instanceof Error ? error.message : String(error) }
|
|
7472
|
+
});
|
|
7473
|
+
return {
|
|
7474
|
+
ok: false,
|
|
7475
|
+
reason: "ca-unavailable"
|
|
7476
|
+
};
|
|
7477
|
+
}
|
|
7478
|
+
const id = crypto$1.randomUUID();
|
|
7479
|
+
const leaf = await issueDeviceLeaf(ca, {
|
|
7480
|
+
credentialId: id,
|
|
7481
|
+
userId: input.userId,
|
|
7482
|
+
publicKeySpki: spki,
|
|
7483
|
+
now: new Date(this.now())
|
|
7484
|
+
});
|
|
7485
|
+
const record = {
|
|
7486
|
+
id,
|
|
7487
|
+
userId: input.userId,
|
|
7488
|
+
deviceName: input.deviceName,
|
|
7489
|
+
platform: input.platform,
|
|
7490
|
+
enrolledVia: input.enrolledVia,
|
|
7491
|
+
fingerprint: leaf.fingerprint,
|
|
7492
|
+
serial: leaf.serialNumber,
|
|
7493
|
+
previousFingerprint: null,
|
|
7494
|
+
renewedAt: null,
|
|
7495
|
+
publicKey: input.publicKeySpki,
|
|
7496
|
+
issuedAt: leaf.notBefore.getTime(),
|
|
7497
|
+
expiresAt: leaf.notAfter.getTime(),
|
|
7498
|
+
lastUsedAt: null,
|
|
7499
|
+
lastUsedFrom: null,
|
|
7500
|
+
revokedAt: null,
|
|
7501
|
+
revokedBy: null
|
|
7502
|
+
};
|
|
7503
|
+
await this.store.insert.mutate({
|
|
7504
|
+
collection: DEVICE_CREDENTIALS_COLLECTION,
|
|
7505
|
+
record: {
|
|
7506
|
+
id,
|
|
7507
|
+
data: { ...record }
|
|
7508
|
+
}
|
|
7509
|
+
});
|
|
7510
|
+
this.records.set(id, record);
|
|
7511
|
+
this.deps.logger?.info("Device credential enrolled", {
|
|
7512
|
+
tags: {
|
|
7513
|
+
credentialId: id,
|
|
7514
|
+
userId: input.userId
|
|
7515
|
+
},
|
|
7516
|
+
meta: {
|
|
7517
|
+
via: input.enrolledVia,
|
|
7518
|
+
platform: input.platform
|
|
7519
|
+
}
|
|
7520
|
+
});
|
|
7521
|
+
return this.issued(record, leaf, ca, user.username);
|
|
7522
|
+
}
|
|
7523
|
+
issued(record, leaf, ca, username) {
|
|
7524
|
+
return {
|
|
7525
|
+
ok: true,
|
|
7526
|
+
credentialId: record.id,
|
|
7527
|
+
userId: record.userId,
|
|
7528
|
+
username,
|
|
7529
|
+
certPem: leaf.certPem,
|
|
7530
|
+
chainPem: ca.certPem,
|
|
7531
|
+
notAfter: leaf.notAfter.getTime()
|
|
7532
|
+
};
|
|
7533
|
+
}
|
|
7534
|
+
/** A single-use nonce bound to `origin`. */
|
|
7535
|
+
issueNonce(origin, partition = "unknown") {
|
|
7536
|
+
this.prune();
|
|
7537
|
+
const own = [...this.nonces.entries()].filter(([, v]) => v.partition === partition);
|
|
7538
|
+
for (const [key] of own.slice(0, Math.max(0, own.length - MAX_NONCES_PER_PARTITION + 1))) this.nonces.delete(key);
|
|
7539
|
+
if (this.nonces.size >= MAX_NONCES_TOTAL) {
|
|
7540
|
+
const oldest = this.nonces.keys().next();
|
|
7541
|
+
if (oldest.done !== true) this.nonces.delete(oldest.value);
|
|
7542
|
+
}
|
|
7543
|
+
const nonce = crypto$1.randomBytes(32).toString("base64url");
|
|
7544
|
+
const expiresAt = this.now() + DEVICE_NONCE_TTL_MS;
|
|
7545
|
+
this.nonces.set(nonce, {
|
|
7546
|
+
origin,
|
|
7547
|
+
expiresAt,
|
|
7548
|
+
partition
|
|
7549
|
+
});
|
|
7550
|
+
return {
|
|
7551
|
+
nonce,
|
|
7552
|
+
expiresAt
|
|
7553
|
+
};
|
|
7554
|
+
}
|
|
7555
|
+
/**
|
|
7556
|
+
* Check a presented certificate + signature. Consumes the nonce on the first
|
|
7557
|
+
* attempt. On success the credential's lastUsed is stamped.
|
|
7558
|
+
*/
|
|
7559
|
+
async authenticate(input) {
|
|
7560
|
+
const bound = this.nonces.get(input.nonce);
|
|
7561
|
+
if (bound === void 0) return this.refuse(input.purpose, "unknown-nonce", {});
|
|
7562
|
+
this.nonces.delete(input.nonce);
|
|
7563
|
+
if (this.now() > bound.expiresAt) return this.refuse(input.purpose, "expired-nonce", {});
|
|
7564
|
+
const caState = await this.deps.ca.lookup();
|
|
7565
|
+
if (caState.state !== "ready") return this.refuse(input.purpose, "no-device-ca", {});
|
|
7566
|
+
const verdict = await verifyDeviceLeaf(caState.ca.certPem, input.certPem, new Date(this.now()));
|
|
7567
|
+
if (!verdict.ok) return this.refuse(input.purpose, verdict.reason, {});
|
|
7568
|
+
const message = buildDeviceSignatureMessage({
|
|
7569
|
+
purpose: input.purpose,
|
|
7570
|
+
origin: bound.origin,
|
|
7571
|
+
nonce: input.nonce
|
|
7572
|
+
});
|
|
7573
|
+
if (!verifyDeviceSignature(verdict.publicKeySpki, message, input.signature)) return this.refuse(input.purpose, "bad-signature", { credentialId: verdict.credentialId });
|
|
7574
|
+
const record = this.records.get(verdict.credentialId);
|
|
7575
|
+
if (record === void 0) return this.refuse(input.purpose, "unknown-credential", { credentialId: verdict.credentialId });
|
|
7576
|
+
const tags = {
|
|
7577
|
+
credentialId: record.id,
|
|
7578
|
+
userId: record.userId
|
|
7579
|
+
};
|
|
7580
|
+
if (record.revokedAt !== null) return this.refuse(input.purpose, "revoked", tags);
|
|
7581
|
+
if (verdict.userId !== null && verdict.userId !== record.userId) return this.refuse(input.purpose, "user-mismatch", tags);
|
|
7582
|
+
if (!verdict.publicKeySpki.equals(Buffer.from(record.publicKey, "base64url"))) return this.refuse(input.purpose, "key-mismatch", tags);
|
|
7583
|
+
if (verdict.fingerprint !== record.fingerprint) {
|
|
7584
|
+
if (!(record.previousFingerprint !== null && verdict.fingerprint === record.previousFingerprint && record.renewedAt !== null && this.now() - record.renewedAt <= 864e5)) return this.refuse(input.purpose, "superseded", tags);
|
|
7585
|
+
}
|
|
7586
|
+
const used = {
|
|
7587
|
+
...record,
|
|
7588
|
+
lastUsedAt: this.now(),
|
|
7589
|
+
lastUsedFrom: input.from ?? record.lastUsedFrom
|
|
7590
|
+
};
|
|
7591
|
+
await this.save(used);
|
|
7592
|
+
return {
|
|
7593
|
+
ok: true,
|
|
7594
|
+
credential: used
|
|
7595
|
+
};
|
|
7596
|
+
}
|
|
7597
|
+
/** A new leaf for the SAME public key, after {@link authenticate} accepted a `renew` proof. */
|
|
7598
|
+
async renew(credential) {
|
|
7599
|
+
const current = this.records.get(credential.id);
|
|
7600
|
+
if (current === void 0) return this.refuse("renew", "unknown-credential", {});
|
|
7601
|
+
if (current.revokedAt !== null) return this.refuse("renew", "revoked", {
|
|
7602
|
+
credentialId: current.id,
|
|
7603
|
+
userId: current.userId
|
|
7604
|
+
});
|
|
7605
|
+
let ca;
|
|
7606
|
+
try {
|
|
7607
|
+
ca = await this.deps.ca.ensure();
|
|
7608
|
+
} catch {
|
|
7609
|
+
return {
|
|
7610
|
+
ok: false,
|
|
7611
|
+
reason: "ca-unavailable"
|
|
7612
|
+
};
|
|
7613
|
+
}
|
|
7614
|
+
const leaf = await issueDeviceLeaf(ca, {
|
|
7615
|
+
credentialId: current.id,
|
|
7616
|
+
userId: current.userId,
|
|
7617
|
+
publicKeySpki: Buffer.from(current.publicKey, "base64url"),
|
|
7618
|
+
now: new Date(this.now())
|
|
7619
|
+
});
|
|
7620
|
+
const updated = {
|
|
7621
|
+
...current,
|
|
7622
|
+
fingerprint: leaf.fingerprint,
|
|
7623
|
+
serial: leaf.serialNumber,
|
|
7624
|
+
previousFingerprint: current.fingerprint,
|
|
7625
|
+
renewedAt: this.now(),
|
|
7626
|
+
issuedAt: leaf.notBefore.getTime(),
|
|
7627
|
+
expiresAt: leaf.notAfter.getTime()
|
|
7628
|
+
};
|
|
7629
|
+
await this.save(updated);
|
|
7630
|
+
this.deps.logger?.info("Device certificate renewed", { tags: {
|
|
7631
|
+
credentialId: updated.id,
|
|
7632
|
+
userId: updated.userId
|
|
7633
|
+
} });
|
|
7634
|
+
return {
|
|
7635
|
+
ok: true,
|
|
7636
|
+
credentialId: updated.id,
|
|
7637
|
+
userId: updated.userId,
|
|
7638
|
+
certPem: leaf.certPem,
|
|
7639
|
+
chainPem: ca.certPem,
|
|
7640
|
+
notAfter: leaf.notAfter.getTime()
|
|
7641
|
+
};
|
|
7642
|
+
}
|
|
7643
|
+
/** Synchronous: the hub asks on every refresh of a credential-bound session. */
|
|
7644
|
+
check(credentialId) {
|
|
7645
|
+
const record = this.records.get(credentialId);
|
|
7646
|
+
if (record === void 0) return "unknown";
|
|
7647
|
+
if (record.revokedAt !== null) return "revoked";
|
|
7648
|
+
return record.expiresAt <= this.now() ? "expired" : "active";
|
|
7649
|
+
}
|
|
7650
|
+
get(credentialId) {
|
|
7651
|
+
return this.records.get(credentialId);
|
|
7652
|
+
}
|
|
7653
|
+
list(input) {
|
|
7654
|
+
return [...this.records.values()].filter((r) => input.userId === void 0 || r.userId === input.userId).filter((r) => input.includeRevoked || r.revokedAt === null).toSorted((a, b) => b.issuedAt - a.issuedAt);
|
|
7655
|
+
}
|
|
7656
|
+
/**
|
|
7657
|
+
* Delete a REVOKED row (admin clean-up). `live` refuses a credential that is
|
|
7658
|
+
* not revoked — a live credential is revoked first, so a delete can never be
|
|
7659
|
+
* the way a device silently stops working.
|
|
7660
|
+
*/
|
|
7661
|
+
async delete(credentialId) {
|
|
7662
|
+
const record = this.records.get(credentialId);
|
|
7663
|
+
if (record === void 0) return "unknown";
|
|
7664
|
+
if (record.revokedAt === null) {
|
|
7665
|
+
this.deps.logger?.warn("Device credential delete refused: still live", { tags: {
|
|
7666
|
+
credentialId,
|
|
7667
|
+
userId: record.userId
|
|
7668
|
+
} });
|
|
7669
|
+
return "live";
|
|
7670
|
+
}
|
|
7671
|
+
this.records.delete(credentialId);
|
|
7672
|
+
await this.store.delete.mutate({
|
|
7673
|
+
collection: DEVICE_CREDENTIALS_COLLECTION,
|
|
7674
|
+
key: credentialId
|
|
7675
|
+
});
|
|
7676
|
+
this.deps.logger?.info("Device credential row deleted", { tags: {
|
|
7677
|
+
credentialId,
|
|
7678
|
+
userId: record.userId
|
|
7679
|
+
} });
|
|
7680
|
+
return "deleted";
|
|
7681
|
+
}
|
|
7682
|
+
/** Revoke; `false` when unknown or already revoked. `revokedBy`: see `formatDeviceRevoker`. */
|
|
7683
|
+
async revoke(credentialId, revokedBy) {
|
|
7684
|
+
const record = this.records.get(credentialId);
|
|
7685
|
+
if (record === void 0 || record.revokedAt !== null) return false;
|
|
7686
|
+
await this.save({
|
|
7687
|
+
...record,
|
|
7688
|
+
revokedAt: this.now(),
|
|
7689
|
+
revokedBy
|
|
7690
|
+
});
|
|
7691
|
+
this.deps.logger?.warn("Device credential revoked", {
|
|
7692
|
+
tags: {
|
|
7693
|
+
credentialId,
|
|
7694
|
+
userId: record.userId
|
|
7695
|
+
},
|
|
7696
|
+
meta: { revokedBy }
|
|
7697
|
+
});
|
|
7698
|
+
return true;
|
|
7699
|
+
}
|
|
7700
|
+
async rename(credentialId, deviceName) {
|
|
7701
|
+
const record = this.records.get(credentialId);
|
|
7702
|
+
if (record === void 0) return false;
|
|
7703
|
+
await this.save({
|
|
7704
|
+
...record,
|
|
7705
|
+
deviceName
|
|
7706
|
+
});
|
|
7707
|
+
return true;
|
|
7708
|
+
}
|
|
7709
|
+
/** Revoke every credential of a user (account deleted). Returns their ids. */
|
|
7710
|
+
async revokeAllForUser(userId, revokedBy) {
|
|
7711
|
+
const ids = [...this.records.values()].filter((r) => r.userId === userId && r.revokedAt === null).map((r) => r.id);
|
|
7712
|
+
for (const id of ids) await this.revoke(id, revokedBy);
|
|
7713
|
+
return ids;
|
|
7714
|
+
}
|
|
7715
|
+
toSummary(record, username) {
|
|
7716
|
+
return {
|
|
7717
|
+
id: record.id,
|
|
7718
|
+
userId: record.userId,
|
|
7719
|
+
username,
|
|
7720
|
+
deviceName: record.deviceName,
|
|
7721
|
+
platform: record.platform,
|
|
7722
|
+
enrolledVia: record.enrolledVia,
|
|
7723
|
+
fingerprint: record.fingerprint,
|
|
7724
|
+
issuedAt: record.issuedAt,
|
|
7725
|
+
expiresAt: record.expiresAt,
|
|
7726
|
+
lastUsedAt: record.lastUsedAt,
|
|
7727
|
+
lastUsedFrom: record.lastUsedFrom,
|
|
7728
|
+
revokedAt: record.revokedAt,
|
|
7729
|
+
revokedBy: record.revokedBy,
|
|
7730
|
+
status: record.revokedAt !== null ? "revoked" : record.expiresAt <= this.now() ? "expired" : "active"
|
|
7731
|
+
};
|
|
7732
|
+
}
|
|
7733
|
+
/** Delete rows revoked or expired more than 90 days ago. */
|
|
7734
|
+
async reap() {
|
|
7735
|
+
const cutoff = this.now() - REAP_RETENTION_MS;
|
|
7736
|
+
const dead = [...this.records.values()].filter((r) => (r.revokedAt ?? r.expiresAt) < cutoff);
|
|
7737
|
+
for (const r of dead) {
|
|
7738
|
+
this.records.delete(r.id);
|
|
7739
|
+
await this.store.delete.mutate({
|
|
7740
|
+
collection: DEVICE_CREDENTIALS_COLLECTION,
|
|
7741
|
+
key: r.id
|
|
7742
|
+
});
|
|
7743
|
+
}
|
|
7744
|
+
return dead.length;
|
|
7745
|
+
}
|
|
7746
|
+
async save(record) {
|
|
7747
|
+
this.records.set(record.id, record);
|
|
7748
|
+
try {
|
|
7749
|
+
await this.store.update.mutate({
|
|
7750
|
+
collection: DEVICE_CREDENTIALS_COLLECTION,
|
|
7751
|
+
id: record.id,
|
|
7752
|
+
data: { ...record }
|
|
7753
|
+
});
|
|
7754
|
+
} catch (error) {
|
|
7755
|
+
this.deps.logger?.error("Device credential write failed — the change holds until restart", {
|
|
7756
|
+
tags: {
|
|
7757
|
+
credentialId: record.id,
|
|
7758
|
+
userId: record.userId
|
|
7759
|
+
},
|
|
7760
|
+
meta: { error: error instanceof Error ? error.message : String(error) }
|
|
7761
|
+
});
|
|
7762
|
+
}
|
|
7763
|
+
}
|
|
7764
|
+
refuse(leg, reason, context) {
|
|
7765
|
+
this.deps.logger?.warn(`Device credential ${leg} refused: ${reason}`, {
|
|
7766
|
+
tags: {
|
|
7767
|
+
...context.credentialId !== void 0 ? { credentialId: context.credentialId } : {},
|
|
7768
|
+
...context.userId !== void 0 ? { userId: context.userId } : {}
|
|
7769
|
+
},
|
|
7770
|
+
meta: {
|
|
7771
|
+
leg,
|
|
7772
|
+
reason,
|
|
7773
|
+
...context.detail !== void 0 ? { detail: context.detail } : {}
|
|
7774
|
+
}
|
|
7775
|
+
});
|
|
7776
|
+
return {
|
|
7777
|
+
ok: false,
|
|
7778
|
+
reason
|
|
7779
|
+
};
|
|
7780
|
+
}
|
|
7781
|
+
prune() {
|
|
7782
|
+
const at = this.now();
|
|
7783
|
+
for (const [k, v] of this.pendingEnrollments) if (at > v.expiresAt) this.pendingEnrollments.delete(k);
|
|
7784
|
+
for (const [k, exp] of this.usedCodes) if (at > exp + 6e5) this.usedCodes.delete(k);
|
|
7785
|
+
for (const [k, v] of this.nonces) if (at > v.expiresAt) this.nonces.delete(k);
|
|
7786
|
+
}
|
|
7787
|
+
};
|
|
7788
|
+
//#endregion
|
|
7083
7789
|
//#region src/auth/auth-audit-log.ts
|
|
7084
7790
|
/**
|
|
7085
7791
|
* The authentication audit trail: what an operator must be able to answer
|
|
@@ -7134,7 +7840,11 @@ var AUTH_AUDIT_ACTIONS = [
|
|
|
7134
7840
|
"admin-token-revoked",
|
|
7135
7841
|
"credentials-revoked",
|
|
7136
7842
|
"agent-addon-update",
|
|
7137
|
-
"agent-addon-update-refused"
|
|
7843
|
+
"agent-addon-update-refused",
|
|
7844
|
+
"device-credential-enrolled",
|
|
7845
|
+
"device-credential-revoked",
|
|
7846
|
+
"device-credential-login",
|
|
7847
|
+
"device-credential-login-refused"
|
|
7138
7848
|
];
|
|
7139
7849
|
var RETENTION_MS = 90 * 864e5;
|
|
7140
7850
|
var MAX_LIST = 500;
|
|
@@ -7260,7 +7970,7 @@ var USERS_COLLECTION$1 = "users";
|
|
|
7260
7970
|
var FIRST_BOOT_ADMIN_USERNAME = "admin";
|
|
7261
7971
|
var FIRST_BOOT_ADMIN_PASSWORD = "changeme";
|
|
7262
7972
|
function parseUser(data) {
|
|
7263
|
-
return parseRecord$
|
|
7973
|
+
return parseRecord$2("user", UserRecordSchema, data);
|
|
7264
7974
|
}
|
|
7265
7975
|
var UserManager = class {
|
|
7266
7976
|
storageAccess;
|
|
@@ -7854,6 +8564,14 @@ async function declareAuthSchema(store) {
|
|
|
7854
8564
|
columns: ["createdBy"]
|
|
7855
8565
|
}]
|
|
7856
8566
|
});
|
|
8567
|
+
await store.declareCollection.mutate({
|
|
8568
|
+
collection: DEVICE_CREDENTIALS_COLLECTION,
|
|
8569
|
+
columns: DEVICE_CREDENTIALS_COLUMNS,
|
|
8570
|
+
indexes: [{
|
|
8571
|
+
name: "idx_device_credentials_user_id",
|
|
8572
|
+
columns: ["userId"]
|
|
8573
|
+
}]
|
|
8574
|
+
});
|
|
7857
8575
|
await store.declareCollection.mutate({
|
|
7858
8576
|
collection: AUTH_AUDIT_COLLECTION,
|
|
7859
8577
|
columns: AUTH_AUDIT_COLUMNS,
|
|
@@ -10751,6 +11469,8 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10751
11469
|
loginSessions = null;
|
|
10752
11470
|
adminTokens = null;
|
|
10753
11471
|
auditLog = null;
|
|
11472
|
+
deviceCredentials = null;
|
|
11473
|
+
deviceCa = null;
|
|
10754
11474
|
constructor() {
|
|
10755
11475
|
super({});
|
|
10756
11476
|
}
|
|
@@ -10780,6 +11500,25 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10780
11500
|
await this.loginSessions.load();
|
|
10781
11501
|
this.adminTokens = new AdminTokenManager(store);
|
|
10782
11502
|
this.auditLog = new AuthAuditLog(store, this.ctx.logger);
|
|
11503
|
+
const deviceUsers = this.userManager;
|
|
11504
|
+
this.deviceCa = new DeviceCaStore({
|
|
11505
|
+
secrets: forOwner === null ? null : forOwner("system"),
|
|
11506
|
+
readSection: async (section) => await this.ctx.settings?.getSection(section) ?? {},
|
|
11507
|
+
writeSection: async (section, patch) => {
|
|
11508
|
+
await this.ctx.settings?.setSection(section, patch);
|
|
11509
|
+
},
|
|
11510
|
+
logger: this.ctx.logger
|
|
11511
|
+
});
|
|
11512
|
+
this.deviceCredentials = new DeviceCredentialManager(store, {
|
|
11513
|
+
ca: this.deviceCa,
|
|
11514
|
+
findUser: async (userId) => {
|
|
11515
|
+
const user = await deviceUsers.findById(userId);
|
|
11516
|
+
return user === null ? null : { username: user.username };
|
|
11517
|
+
},
|
|
11518
|
+
selfEnrollmentAllowed: async () => (await this.readDeviceCertificateSettings()).selfEnrollment === "allowed",
|
|
11519
|
+
logger: this.ctx.logger
|
|
11520
|
+
});
|
|
11521
|
+
await this.deviceCredentials.load();
|
|
10783
11522
|
try {
|
|
10784
11523
|
await reconcileAdminAccountAtBoot({
|
|
10785
11524
|
users: this.userManager,
|
|
@@ -10879,6 +11618,7 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10879
11618
|
if (!this.userManager) throw new Error("User management not available");
|
|
10880
11619
|
await this.userManager.resetPassword(input.id, input.newPassword);
|
|
10881
11620
|
await this.loginSessions?.revokeAllForUser(input.id, { reason: "password-reset" });
|
|
11621
|
+
await this.revokeDeviceCredentialsForReset(input.id);
|
|
10882
11622
|
return { success: true };
|
|
10883
11623
|
},
|
|
10884
11624
|
setUserScopes: async (input) => {
|
|
@@ -11003,13 +11743,154 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
11003
11743
|
};
|
|
11004
11744
|
this.ctx.logger.info("registered auth-provider + user-management capabilities");
|
|
11005
11745
|
attachUserManagementInternal(userMgmt, this.buildInternal());
|
|
11006
|
-
return [
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11746
|
+
return [
|
|
11747
|
+
{
|
|
11748
|
+
capability: authProviderCapability,
|
|
11749
|
+
provider: authProvider
|
|
11750
|
+
},
|
|
11751
|
+
{
|
|
11752
|
+
capability: userManagementCapability,
|
|
11753
|
+
provider: userMgmt
|
|
11754
|
+
},
|
|
11755
|
+
{
|
|
11756
|
+
capability: deviceCredentialsCapability,
|
|
11757
|
+
provider: this.buildDeviceCredentialsProvider()
|
|
11758
|
+
}
|
|
11759
|
+
];
|
|
11760
|
+
}
|
|
11761
|
+
async readDeviceCertificateSettings() {
|
|
11762
|
+
return { selfEnrollment: (await this.ctx.settings?.getSection("deviceCertificates") ?? {})["selfEnrollment"] === "disabled" ? "disabled" : "allowed" };
|
|
11763
|
+
}
|
|
11764
|
+
async revokeDeviceCredentialsForReset(userId, by = "password-reset") {
|
|
11765
|
+
const ids = await this.deviceCredentials?.revokeAllForUser(userId, by) ?? [];
|
|
11766
|
+
for (const id of ids) {
|
|
11767
|
+
const ended = await this.loginSessions?.revokeByCredential(id) ?? 0;
|
|
11768
|
+
await this.auditLog?.record({
|
|
11769
|
+
action: "device-credential-revoked",
|
|
11770
|
+
userId,
|
|
11771
|
+
detail: {
|
|
11772
|
+
credentialId: id,
|
|
11773
|
+
by,
|
|
11774
|
+
sessionsEnded: ended
|
|
11775
|
+
}
|
|
11776
|
+
});
|
|
11777
|
+
}
|
|
11778
|
+
}
|
|
11779
|
+
/** The admin cap (`device-credentials`): list, enroll-by-QR, revoke, rename, settings. */
|
|
11780
|
+
buildDeviceCredentialsProvider() {
|
|
11781
|
+
const manager = () => {
|
|
11782
|
+
if (this.deviceCredentials === null) throw new Error("Device credentials not available");
|
|
11783
|
+
return this.deviceCredentials;
|
|
11784
|
+
};
|
|
11785
|
+
const hubOrigins = async () => {
|
|
11786
|
+
try {
|
|
11787
|
+
const book = await this.ctx.api?.localNetwork?.getAddresses?.query({});
|
|
11788
|
+
if (!book) return [];
|
|
11789
|
+
const ordered = [book.roles.publicEndpoint.resolved, ...book.roles.appConnections.resolved];
|
|
11790
|
+
return [...new Set(ordered.filter((o) => typeof o === "string"))].filter((o) => !/^https?:\/\/(127\.|localhost|\[::1\])/i.test(o));
|
|
11791
|
+
} catch (err) {
|
|
11792
|
+
this.ctx.logger.warn("Enrollment link: the hub address could not be resolved", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
11793
|
+
return [];
|
|
11794
|
+
}
|
|
11795
|
+
};
|
|
11796
|
+
return {
|
|
11797
|
+
listDeviceCredentials: async (input) => {
|
|
11798
|
+
const users = new Map((await this.userManager?.listAll() ?? []).map((u) => [u.id, u.username]));
|
|
11799
|
+
return manager().list({
|
|
11800
|
+
...input.userId !== void 0 ? { userId: input.userId } : {},
|
|
11801
|
+
includeRevoked: input.includeRevoked
|
|
11802
|
+
}).map((r) => manager().toSummary(r, users.get(r.userId) ?? null));
|
|
11803
|
+
},
|
|
11804
|
+
startEnrollment: async (input) => {
|
|
11805
|
+
if (await this.userManager?.findById(input.userId) == null) throw new Error(`User not found: ${input.userId}`);
|
|
11806
|
+
const started = manager().startEnrollment({
|
|
11807
|
+
userId: input.userId,
|
|
11808
|
+
deviceName: input.deviceName,
|
|
11809
|
+
createdBy: input.caller.userId
|
|
11810
|
+
});
|
|
11811
|
+
const origins = await hubOrigins();
|
|
11812
|
+
const hub = origins[0];
|
|
11813
|
+
await this.auditLog?.record({
|
|
11814
|
+
action: "device-credential-enrolled",
|
|
11815
|
+
userId: input.userId,
|
|
11816
|
+
detail: {
|
|
11817
|
+
stage: "code-issued",
|
|
11818
|
+
by: input.caller.userId
|
|
11819
|
+
}
|
|
11820
|
+
});
|
|
11821
|
+
return {
|
|
11822
|
+
code: started.code,
|
|
11823
|
+
expiresAt: started.expiresAt,
|
|
11824
|
+
enrollUrl: buildDeviceEnrollmentLink({
|
|
11825
|
+
...hub !== void 0 ? { hub } : {},
|
|
11826
|
+
code: started.code
|
|
11827
|
+
}),
|
|
11828
|
+
hubOrigin: hub ?? null,
|
|
11829
|
+
hubOrigins: [...origins]
|
|
11830
|
+
};
|
|
11831
|
+
},
|
|
11832
|
+
revokeDeviceCredential: async (input) => {
|
|
11833
|
+
const record = manager().get(input.id);
|
|
11834
|
+
if (record === void 0) throw new Error(`Device credential not found: ${input.id}`);
|
|
11835
|
+
const revoked = await manager().revoke(input.id, formatDeviceRevoker({
|
|
11836
|
+
kind: "admin",
|
|
11837
|
+
userId: input.caller.userId
|
|
11838
|
+
}));
|
|
11839
|
+
const sessionsEnded = await this.loginSessions?.revokeByCredential(input.id) ?? 0;
|
|
11840
|
+
if (revoked) await this.auditLog?.record({
|
|
11841
|
+
action: "device-credential-revoked",
|
|
11842
|
+
userId: record.userId,
|
|
11843
|
+
detail: {
|
|
11844
|
+
credentialId: input.id,
|
|
11845
|
+
by: input.caller.userId,
|
|
11846
|
+
sessionsEnded
|
|
11847
|
+
}
|
|
11848
|
+
});
|
|
11849
|
+
return {
|
|
11850
|
+
revoked,
|
|
11851
|
+
sessionsEnded,
|
|
11852
|
+
status: "revoked"
|
|
11853
|
+
};
|
|
11854
|
+
},
|
|
11855
|
+
deleteDeviceCredential: async (input) => {
|
|
11856
|
+
const record = manager().get(input.id);
|
|
11857
|
+
const result = await manager().delete(input.id);
|
|
11858
|
+
if (result === "deleted" && record !== void 0) await this.auditLog?.record({
|
|
11859
|
+
action: "device-credential-revoked",
|
|
11860
|
+
userId: record.userId,
|
|
11861
|
+
detail: {
|
|
11862
|
+
credentialId: input.id,
|
|
11863
|
+
stage: "row-deleted",
|
|
11864
|
+
by: input.caller.userId
|
|
11865
|
+
}
|
|
11866
|
+
});
|
|
11867
|
+
return { result };
|
|
11868
|
+
},
|
|
11869
|
+
renameDeviceCredential: async (input) => ({ renamed: await manager().rename(input.id, input.deviceName) }),
|
|
11870
|
+
getDeviceCaStatus: async () => {
|
|
11871
|
+
if (this.deviceCa === null) return {
|
|
11872
|
+
state: "unavailable",
|
|
11873
|
+
reason: "not initialised"
|
|
11874
|
+
};
|
|
11875
|
+
const found = await this.deviceCa.lookup();
|
|
11876
|
+
if (found.state !== "ready") return found.state === "absent" ? { state: "absent" } : {
|
|
11877
|
+
state: "unavailable",
|
|
11878
|
+
reason: found.reason
|
|
11879
|
+
};
|
|
11880
|
+
const x509 = new X509Certificate(found.ca.certPem);
|
|
11881
|
+
return {
|
|
11882
|
+
state: "ready",
|
|
11883
|
+
subject: x509.subject,
|
|
11884
|
+
fingerprintSha256: x509.fingerprint256,
|
|
11885
|
+
validTo: x509.validTo
|
|
11886
|
+
};
|
|
11887
|
+
},
|
|
11888
|
+
getDeviceCertificateSettings: async () => this.readDeviceCertificateSettings(),
|
|
11889
|
+
setDeviceCertificateSettings: async (input) => {
|
|
11890
|
+
await this.ctx.settings?.setSection("deviceCertificates", { selfEnrollment: input.selfEnrollment });
|
|
11891
|
+
return this.readDeviceCertificateSettings();
|
|
11892
|
+
}
|
|
11893
|
+
};
|
|
11013
11894
|
}
|
|
11014
11895
|
/**
|
|
11015
11896
|
* The hub-internal surface (`user-management-internal.ts`): operations no
|
|
@@ -11020,6 +11901,10 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
11020
11901
|
if (this.loginSessions === null) throw new Error("Login sessions not available");
|
|
11021
11902
|
return this.loginSessions;
|
|
11022
11903
|
};
|
|
11904
|
+
const devices = () => {
|
|
11905
|
+
if (this.deviceCredentials === null) throw new Error("Device credentials not available");
|
|
11906
|
+
return this.deviceCredentials;
|
|
11907
|
+
};
|
|
11023
11908
|
return {
|
|
11024
11909
|
sessions: {
|
|
11025
11910
|
create: (input) => sessions().create(input),
|
|
@@ -11030,7 +11915,20 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
11030
11915
|
revoke: (id, reason) => sessions().revoke(id, reason),
|
|
11031
11916
|
revokeAllForUser: (userId, options) => sessions().revokeAllForUser(userId, options),
|
|
11032
11917
|
listForUser: (userId) => sessions().listForUser(userId),
|
|
11033
|
-
listActive: () => sessions().listActive()
|
|
11918
|
+
listActive: () => sessions().listActive(),
|
|
11919
|
+
revokeByCredential: (credentialId, reason) => sessions().revokeByCredential(credentialId, reason),
|
|
11920
|
+
peekByRefreshToken: (token) => sessions().peekByRefreshToken(token)
|
|
11921
|
+
},
|
|
11922
|
+
deviceCredentials: {
|
|
11923
|
+
completeEnrollment: (input) => devices().completeEnrollment(input),
|
|
11924
|
+
enrollSelf: (input) => devices().enrollSelf(input),
|
|
11925
|
+
issueNonce: (origin) => devices().issueNonce(origin),
|
|
11926
|
+
authenticate: (input) => devices().authenticate(input),
|
|
11927
|
+
renew: (credential) => devices().renew(credential),
|
|
11928
|
+
check: (id) => devices().check(id),
|
|
11929
|
+
get: (id) => devices().get(id),
|
|
11930
|
+
revoke: (id, by) => devices().revoke(id, by),
|
|
11931
|
+
list: (input) => devices().list(input)
|
|
11034
11932
|
},
|
|
11035
11933
|
adminTokens: {
|
|
11036
11934
|
create: async (input) => {
|
|
@@ -11068,6 +11966,7 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
11068
11966
|
...input.keepSessionId !== void 0 ? { exceptSessionId: input.keepSessionId } : {},
|
|
11069
11967
|
reason: "password-changed"
|
|
11070
11968
|
});
|
|
11969
|
+
await this.revokeDeviceCredentialsForReset(input.userId, "password-changed");
|
|
11071
11970
|
},
|
|
11072
11971
|
linkExternalIdentity: async (input) => {
|
|
11073
11972
|
if (!this.externalIdentities || !this.userManager) throw new Error("External sign-in linking not available");
|
|
@@ -11094,6 +11993,9 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
11094
11993
|
for (const session of await this.oauthSessionManager?.list() ?? []) if (session.userId === userId && session.revokedAt === null) await this.oauthSessionManager?.markRevoked(session.id);
|
|
11095
11994
|
}],
|
|
11096
11995
|
["admin tokens", async () => this.adminTokens?.revokeAllCreatedBy(userId)],
|
|
11996
|
+
["device credentials", async () => {
|
|
11997
|
+
for (const id of await this.deviceCredentials?.revokeAllForUser(userId, "user-deleted") ?? []) await this.loginSessions?.revokeByCredential(id);
|
|
11998
|
+
}],
|
|
11097
11999
|
["totp", async () => this.totpManager?.disable(userId)],
|
|
11098
12000
|
["sign-in links", async () => this.externalIdentities?.removeAllForUser(userId)]
|
|
11099
12001
|
];
|
|
@@ -11123,6 +12025,7 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
11123
12025
|
const logins = await this.loginSessions?.reap() ?? 0;
|
|
11124
12026
|
await this.adminTokens?.reap();
|
|
11125
12027
|
await this.auditLog?.reap();
|
|
12028
|
+
await this.deviceCredentials?.reap();
|
|
11126
12029
|
if (tokens > 0 || sessions > 0 || logins > 0) this.ctx.logger.info("auth retention swept", { meta: {
|
|
11127
12030
|
expiredTokens: tokens,
|
|
11128
12031
|
revokedSessions: sessions,
|
|
@@ -11148,7 +12051,9 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
11148
12051
|
this.loginSessions = null;
|
|
11149
12052
|
this.adminTokens = null;
|
|
11150
12053
|
this.auditLog = null;
|
|
12054
|
+
this.deviceCredentials = null;
|
|
12055
|
+
this.deviceCa = null;
|
|
11151
12056
|
}
|
|
11152
12057
|
};
|
|
11153
12058
|
//#endregion
|
|
11154
|
-
export { LocalAuthAddon, LocalAuthAddon as default,
|
|
12059
|
+
export { LocalAuthAddon, LocalAuthAddon as default, AuthManager as _, attachUserManagementInternal as a, isSettingsReadUnavailable as b, DeviceCredentialManager as c, ADMIN_TOKEN_MAX_LIFETIME_MS as d, ADMIN_TOKEN_PREFIX as f, ScopedTokenManager as g, REFRESH_TOKEN_PREFIX as h, USER_MANAGEMENT_INTERNAL as i, PREVIOUS_LEAF_GRACE_MS as l, LoginSessionManager as m, FIRST_BOOT_ADMIN_USERNAME as n, getUserManagementInternal as o, AdminTokenManager as p, UserManager as r, AuthAuditLog as s, FIRST_BOOT_ADMIN_PASSWORD as t, DeviceCaStore as u, parseTokenExpiry as v, ApiKeyManager as x, SettingsReadUnavailableError as y };
|