@camstack/system 1.2.381 → 1.2.382

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.
Files changed (114) hide show
  1. package/dist/addon-runner.js +4 -2
  2. package/dist/addon-runner.mjs +4 -2
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/auth/admin-token-manager.d.ts +75 -0
  6. package/dist/auth/api-key-manager.d.ts +1 -14
  7. package/dist/auth/auth-audit-log.d.ts +59 -0
  8. package/dist/auth/auth-manager.d.ts +28 -16
  9. package/dist/auth/cidr.d.ts +22 -0
  10. package/dist/auth/login-session-manager.d.ts +158 -0
  11. package/dist/auth/user-management-internal.d.ts +69 -0
  12. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  13. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  14. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  15. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  16. package/dist/builtins/alerts/alerts.addon.js +1 -1
  17. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  18. package/dist/builtins/autotrack/index.js +1 -1
  19. package/dist/builtins/autotrack/index.mjs +1 -1
  20. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +14 -0
  21. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +291 -3631
  22. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +284 -3617
  23. package/dist/builtins/backup-orchestrator/encryption-key-store.d.ts +36 -0
  24. package/dist/builtins/backup-orchestrator/manifest-store.d.ts +19 -1
  25. package/dist/builtins/backup-orchestrator/restore-archive.d.ts +24 -0
  26. package/dist/builtins/calendars/calendars.addon.js +1 -1
  27. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  28. package/dist/builtins/camera-grid/index.js +1 -1
  29. package/dist/builtins/camera-grid/index.mjs +1 -1
  30. package/dist/builtins/composer/composer.addon.js +2 -2
  31. package/dist/builtins/composer/composer.addon.mjs +2 -2
  32. package/dist/builtins/console-logging/index.js +1 -1
  33. package/dist/builtins/console-logging/index.mjs +1 -1
  34. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  35. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  36. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  37. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  38. package/dist/builtins/hub-forwarder/index.js +1 -1
  39. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  40. package/dist/builtins/keychain/index.js +1108 -733
  41. package/dist/builtins/keychain/index.mjs +1108 -733
  42. package/dist/builtins/keychain/keychain-reset-trigger.d.ts +25 -0
  43. package/dist/builtins/keychain/keychain-service.d.ts +36 -3
  44. package/dist/builtins/keychain/keychain-store.d.ts +24 -1
  45. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  46. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  47. package/dist/builtins/local-auth/local-auth.addon.d.ts +16 -0
  48. package/dist/builtins/local-auth/local-auth.addon.js +1021 -118
  49. package/dist/builtins/local-auth/local-auth.addon.mjs +1034 -141
  50. package/dist/builtins/local-network/local-network.addon.js +3 -3
  51. package/dist/builtins/local-network/local-network.addon.mjs +3 -3
  52. package/dist/builtins/loki-logging/index.js +97 -29
  53. package/dist/builtins/loki-logging/index.mjs +97 -29
  54. package/dist/builtins/loki-logging/loki-logging.addon.d.ts +7 -23
  55. package/dist/builtins/loki-logging/retire-enabled-flag.d.ts +23 -0
  56. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  57. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  58. package/dist/builtins/platform-probe/index.js +1 -1
  59. package/dist/builtins/platform-probe/index.mjs +1 -1
  60. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  61. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  62. package/dist/builtins/snapshot/index.js +1 -1
  63. package/dist/builtins/snapshot/index.mjs +1 -1
  64. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  65. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  66. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  67. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  68. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -2
  69. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -2
  70. package/dist/builtins/system-backup/backup-encryption.d.ts +57 -0
  71. package/dist/builtins/system-backup/system-backup.service.d.ts +16 -1
  72. package/dist/builtins/system-config/cluster-secret-keeper.d.ts +7 -0
  73. package/dist/builtins/system-config/system-config.addon.d.ts +13 -3
  74. package/dist/builtins/system-config/system-config.addon.js +324 -38
  75. package/dist/builtins/system-config/system-config.addon.mjs +312 -39
  76. package/dist/builtins/winston-logging/index.js +1 -1
  77. package/dist/builtins/winston-logging/index.mjs +1 -1
  78. package/dist/{child-cap-dispatch-hf-Gce_4.js → child-cap-dispatch-BrMdKHfp.js} +6 -3
  79. package/dist/{child-cap-dispatch-BTe4eKep.mjs → child-cap-dispatch-WsYPNvnK.mjs} +10 -7
  80. package/dist/{cluster-secret-BUiG8DCo.js → cluster-secret-BEPLh3LL.js} +1 -1
  81. package/dist/{cluster-secret-DgJTZ4qY.mjs → cluster-secret-DhPdZ0VA.mjs} +1 -1
  82. package/dist/{composition-sources-4SmsP0O4.js → composition-sources-C6ZynCZ2.js} +1 -1
  83. package/dist/{composition-sources-CgOQVIwa.mjs → composition-sources-XqRX51db.mjs} +1 -1
  84. package/dist/{dist-BC0GnGeg.mjs → dist-CP5TTjwd.mjs} +1710 -158
  85. package/dist/{dist-BugrtARh.js → dist-tnAO12ak.js} +1721 -157
  86. package/dist/{event-loop-stall-monitor-CnQ-K6Gn.js → event-loop-stall-monitor-9kR0sAPS.js} +15 -15
  87. package/dist/{event-loop-stall-monitor-GgpnfAC0.mjs → event-loop-stall-monitor-WkssIXjq.mjs} +15 -15
  88. package/dist/{hub-cap-forward-CH6_VWVO.mjs → hub-cap-forward-CDiN1p98.mjs} +18 -2
  89. package/dist/{hub-cap-forward-COCmYaup.js → hub-cap-forward-W8oEm4ME.js} +23 -1
  90. package/dist/index.d.ts +16 -1
  91. package/dist/index.js +3341 -79
  92. package/dist/index.mjs +3567 -354
  93. package/dist/{jwt-signing-key-CsuVKA1W.mjs → jwt-signing-key-CAbozwvF.mjs} +1 -1
  94. package/dist/{jwt-signing-key-CjoZcol8.js → jwt-signing-key-Dgf8sIXY.js} +1 -1
  95. package/dist/kernel/index.d.ts +12 -0
  96. package/dist/kernel/moleculer/broker-factory.d.ts +8 -1
  97. package/dist/kernel/moleculer/child-addon-call-dispatch.d.ts +1 -0
  98. package/dist/kernel/moleculer/hub-service.d.ts +9 -0
  99. package/dist/kernel/moleculer/mesh-access-middleware.d.ts +28 -0
  100. package/dist/kernel/moleculer/mesh-auth-mode.d.ts +64 -0
  101. package/dist/kernel/moleculer/mesh-authenticator.d.ts +71 -0
  102. package/dist/kernel/moleculer/mesh-auto-enforce.d.ts +26 -0
  103. package/dist/kernel/moleculer/mesh-join-client.d.ts +51 -0
  104. package/dist/kernel/moleculer/mesh-join-proof.d.ts +39 -0
  105. package/dist/kernel/system-settings-policy.d.ts +4 -3
  106. package/dist/kernel/transport/hub-cap-forward.d.ts +14 -3
  107. package/dist/kernel/transport/index.d.ts +1 -1
  108. package/dist/restore-archive-BV39nY46.mjs +4045 -0
  109. package/dist/restore-archive-CbvrfPuB.js +4139 -0
  110. package/dist/{retired-settings-keys-DCuetwOt.js → retired-settings-keys-4bHpL-0m.js} +1 -1
  111. package/dist/{retired-settings-keys-MCVhxPkJ.mjs → retired-settings-keys-CzpIkwS-.mjs} +1 -1
  112. package/dist/{tls-CJHogwTl.mjs → tls-4cRH24Id.mjs} +3 -3
  113. package/package.json +1 -1
  114. package/dist/{tls-CCMX7MjX.js → tls-CljF057D.js} +1 -1
@@ -1,13 +1,13 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Bn as userManagementCapability, J as authProviderCapability, R as ScopedTokenSchema, U as UserRecordSchema, lr as BaseAddon, r as ApiKeyRecordSchema } from "../../dist-BC0GnGeg.mjs";
3
- import { a as loadJwtSigningKey, i as getProcessJwtSigningKeyState, o as processJwtSigningKey, s as setProcessJwtSigningKey, t as JWT_SIGNING_KEY_REF } from "../../jwt-signing-key-CsuVKA1W.mjs";
4
- import * as crypto$2 from "node:crypto";
2
+ import { Vn as userManagementCapability, W as UserRecordSchema, Y as authProviderCapability, r as ApiKeyRecordSchema, ur as BaseAddon, z as ScopedTokenSchema } from "../../dist-CP5TTjwd.mjs";
3
+ import { a as loadJwtSigningKey, i as getProcessJwtSigningKeyState, o as processJwtSigningKey, s as setProcessJwtSigningKey, t as JWT_SIGNING_KEY_REF } from "../../jwt-signing-key-CAbozwvF.mjs";
4
+ import * as crypto$1 from "node:crypto";
5
5
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
6
6
  import nodeCrypto from "crypto";
7
7
  import * as path$1 from "node:path";
8
8
  import * as fs from "node:fs";
9
9
  //#region src/auth/parse-record.ts
10
- function parseRecord(kind, schema, data) {
10
+ function parseRecord$1(kind, schema, data) {
11
11
  try {
12
12
  return schema.parse(data);
13
13
  } catch (err) {
@@ -19,64 +19,26 @@ function parseRecord(kind, schema, data) {
19
19
  }
20
20
  //#endregion
21
21
  //#region src/auth/api-key-manager.ts
22
+ /**
23
+ * Legacy API keys — LIST and REVOKE only.
24
+ *
25
+ * No boundary accepts one (the tRPC context has no branch for them) and
26
+ * nothing creates them any more: what is left is the rows a hub may still
27
+ * hold, so an operator can see and delete them. Scoped tokens (`cst_`) are
28
+ * the credential for scripts and integrations.
29
+ */
22
30
  var API_KEYS_COLLECTION$1 = "api_keys";
23
31
  function parseApiKey(data) {
24
- return parseRecord("api-key", ApiKeyRecordSchema, data);
32
+ return parseRecord$1("api-key", ApiKeyRecordSchema, data);
25
33
  }
26
34
  var ApiKeyManager = class {
27
35
  storageAccess;
28
- auth;
29
- constructor(storageAccess, auth) {
36
+ constructor(storageAccess) {
30
37
  this.storageAccess = storageAccess;
31
- this.auth = auth;
32
38
  }
33
39
  get store() {
34
40
  return this.storageAccess.getStore();
35
41
  }
36
- async create(input) {
37
- const { token: rawToken, hash, prefix } = this.auth.generateApiKey();
38
- const now = Date.now();
39
- const record = {
40
- id: crypto$2.randomUUID(),
41
- label: input.label,
42
- isAdmin: input.isAdmin ?? false,
43
- allowedProviders: input.allowedProviders ?? "*",
44
- allowedDevices: input.allowedDevices ?? {},
45
- tokenHash: hash,
46
- tokenPrefix: prefix,
47
- createdAt: now
48
- };
49
- await this.store.insert.mutate({
50
- collection: API_KEYS_COLLECTION$1,
51
- record: {
52
- id: record.id,
53
- data: { ...record }
54
- }
55
- });
56
- return {
57
- record,
58
- token: rawToken
59
- };
60
- }
61
- async validateToken(token) {
62
- const allKeys = await this.store.query.query({ collection: API_KEYS_COLLECTION$1 });
63
- for (const entry of allKeys) {
64
- const record = parseApiKey(entry.data);
65
- if (this.auth.validateApiKey(token, record.tokenHash)) {
66
- const updatedData = {
67
- ...record,
68
- lastUsedAt: Date.now()
69
- };
70
- await this.store.update.mutate({
71
- collection: API_KEYS_COLLECTION$1,
72
- id: record.id,
73
- data: { ...updatedData }
74
- });
75
- return updatedData;
76
- }
77
- }
78
- return null;
79
- }
80
42
  async listAll() {
81
43
  return (await this.store.query.query({ collection: API_KEYS_COLLECTION$1 })).map((r) => {
82
44
  const { tokenHash: _th, ...rest } = parseApiKey(r.data);
@@ -189,7 +151,7 @@ function genSalt(rounds, seed_length, callback) {
189
151
  * @returns {!Promise} If `callback` has been omitted
190
152
  * @throws {Error} If `callback` is present but not a function
191
153
  */
192
- function hash(password, salt, callback, progressCallback) {
154
+ function hash$1(password, salt, callback, progressCallback) {
193
155
  function _async(callback) {
194
156
  if (typeof password === "string" && typeof salt === "number") genSalt(salt, function(err, salt) {
195
157
  _hash(password, salt, callback, progressCallback);
@@ -242,7 +204,7 @@ function compare(password, hashValue, callback, progressCallback) {
242
204
  nextTick(callback.bind(this, null, false));
243
205
  return;
244
206
  }
245
- hash(password, hashValue.substring(0, 29), function(err, comp) {
207
+ hash$1(password, hashValue.substring(0, 29), function(err, comp) {
246
208
  if (err) callback(err);
247
209
  else callback(null, safeStringCompare(comp, hashValue));
248
210
  }, progressCallback);
@@ -2113,14 +2075,14 @@ var require_buffer_equal_constant_time = /* @__PURE__ */ __commonJSMin(((exports
2113
2075
  //#region ../../node_modules/jwa/index.js
2114
2076
  var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2115
2077
  var Buffer = require_safe_buffer().Buffer;
2116
- var crypto$1 = __require("crypto");
2078
+ var crypto$2 = __require("crypto");
2117
2079
  var formatEcdsa = require_ecdsa_sig_formatter();
2118
2080
  var util$2 = __require("util");
2119
2081
  var MSG_INVALID_ALGORITHM = "\"%s\" is not a valid algorithm.\n Supported algorithms are:\n \"HS256\", \"HS384\", \"HS512\", \"RS256\", \"RS384\", \"RS512\", \"PS256\", \"PS384\", \"PS512\", \"ES256\", \"ES384\", \"ES512\" and \"none\".";
2120
2082
  var MSG_INVALID_SECRET = "secret must be a string or buffer";
2121
2083
  var MSG_INVALID_VERIFIER_KEY = "key must be a string or a buffer";
2122
2084
  var MSG_INVALID_SIGNER_KEY = "key must be a string, a buffer or an object";
2123
- var supportsKeyObjects = typeof crypto$1.createPublicKey === "function";
2085
+ var supportsKeyObjects = typeof crypto$2.createPublicKey === "function";
2124
2086
  if (supportsKeyObjects) {
2125
2087
  MSG_INVALID_VERIFIER_KEY += " or a KeyObject";
2126
2088
  MSG_INVALID_SECRET += "or a KeyObject";
@@ -2173,14 +2135,14 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2173
2135
  return function sign(thing, secret) {
2174
2136
  checkIsSecretKey(secret);
2175
2137
  thing = normalizeInput(thing);
2176
- var hmac = crypto$1.createHmac("sha" + bits, secret);
2138
+ var hmac = crypto$2.createHmac("sha" + bits, secret);
2177
2139
  return fromBase64((hmac.update(thing), hmac.digest("base64")));
2178
2140
  };
2179
2141
  }
2180
2142
  var bufferEqual;
2181
- var timingSafeEqual = "timingSafeEqual" in crypto$1 ? function timingSafeEqual(a, b) {
2143
+ var timingSafeEqual = "timingSafeEqual" in crypto$2 ? function timingSafeEqual(a, b) {
2182
2144
  if (a.byteLength !== b.byteLength) return false;
2183
- return crypto$1.timingSafeEqual(a, b);
2145
+ return crypto$2.timingSafeEqual(a, b);
2184
2146
  } : function timingSafeEqual(a, b) {
2185
2147
  if (!bufferEqual) bufferEqual = require_buffer_equal_constant_time();
2186
2148
  return bufferEqual(a, b);
@@ -2195,7 +2157,7 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2195
2157
  return function sign(thing, privateKey) {
2196
2158
  checkIsPrivateKey(privateKey);
2197
2159
  thing = normalizeInput(thing);
2198
- var signer = crypto$1.createSign("RSA-SHA" + bits);
2160
+ var signer = crypto$2.createSign("RSA-SHA" + bits);
2199
2161
  return fromBase64((signer.update(thing), signer.sign(privateKey, "base64")));
2200
2162
  };
2201
2163
  }
@@ -2204,7 +2166,7 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2204
2166
  checkIsPublicKey(publicKey);
2205
2167
  thing = normalizeInput(thing);
2206
2168
  signature = toBase64(signature);
2207
- var verifier = crypto$1.createVerify("RSA-SHA" + bits);
2169
+ var verifier = crypto$2.createVerify("RSA-SHA" + bits);
2208
2170
  verifier.update(thing);
2209
2171
  return verifier.verify(publicKey, signature, "base64");
2210
2172
  };
@@ -2213,11 +2175,11 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2213
2175
  return function sign(thing, privateKey) {
2214
2176
  checkIsPrivateKey(privateKey);
2215
2177
  thing = normalizeInput(thing);
2216
- var signer = crypto$1.createSign("RSA-SHA" + bits);
2178
+ var signer = crypto$2.createSign("RSA-SHA" + bits);
2217
2179
  return fromBase64((signer.update(thing), signer.sign({
2218
2180
  key: privateKey,
2219
- padding: crypto$1.constants.RSA_PKCS1_PSS_PADDING,
2220
- saltLength: crypto$1.constants.RSA_PSS_SALTLEN_DIGEST
2181
+ padding: crypto$2.constants.RSA_PKCS1_PSS_PADDING,
2182
+ saltLength: crypto$2.constants.RSA_PSS_SALTLEN_DIGEST
2221
2183
  }, "base64")));
2222
2184
  };
2223
2185
  }
@@ -2226,12 +2188,12 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
2226
2188
  checkIsPublicKey(publicKey);
2227
2189
  thing = normalizeInput(thing);
2228
2190
  signature = toBase64(signature);
2229
- var verifier = crypto$1.createVerify("RSA-SHA" + bits);
2191
+ var verifier = crypto$2.createVerify("RSA-SHA" + bits);
2230
2192
  verifier.update(thing);
2231
2193
  return verifier.verify({
2232
2194
  key: publicKey,
2233
- padding: crypto$1.constants.RSA_PKCS1_PSS_PADDING,
2234
- saltLength: crypto$1.constants.RSA_PSS_SALTLEN_DIGEST
2195
+ padding: crypto$2.constants.RSA_PKCS1_PSS_PADDING,
2196
+ saltLength: crypto$2.constants.RSA_PSS_SALTLEN_DIGEST
2235
2197
  }, signature, "base64");
2236
2198
  };
2237
2199
  }
@@ -4156,7 +4118,7 @@ var require_verify = /* @__PURE__ */ __commonJSMin(((exports, module) => {
4156
4118
  var validateAsymmetricKey = require_validateAsymmetricKey();
4157
4119
  var PS_SUPPORTED = require_psSupported();
4158
4120
  var jws = require_jws();
4159
- var { KeyObject: KeyObject$1, createSecretKey: createSecretKey$1, createPublicKey } = __require("crypto");
4121
+ var { KeyObject: KeyObject$1, createSecretKey: createSecretKey$1, createPublicKey: createPublicKey$1 } = __require("crypto");
4160
4122
  var PUB_KEY_ALGS = [
4161
4123
  "RS256",
4162
4124
  "RS384",
@@ -4224,7 +4186,7 @@ var require_verify = /* @__PURE__ */ __commonJSMin(((exports, module) => {
4224
4186
  if (hasSignature && !secretOrPublicKey) return done(new JsonWebTokenError("secret or public key must be provided"));
4225
4187
  if (!hasSignature && !options.algorithms) return done(new JsonWebTokenError("please specify \"none\" in \"algorithms\" to verify unsigned tokens"));
4226
4188
  if (secretOrPublicKey != null && !(secretOrPublicKey instanceof KeyObject$1)) try {
4227
- secretOrPublicKey = createPublicKey(secretOrPublicKey);
4189
+ secretOrPublicKey = createPublicKey$1(secretOrPublicKey);
4228
4190
  } catch (_) {
4229
4191
  try {
4230
4192
  secretOrPublicKey = createSecretKey$1(typeof secretOrPublicKey === "string" ? Buffer.from(secretOrPublicKey) : secretOrPublicKey);
@@ -6221,7 +6183,15 @@ var AuthManager = class {
6221
6183
  get jwtSecret() {
6222
6184
  return this.signingKey.current();
6223
6185
  }
6224
- signToken(payload) {
6186
+ /**
6187
+ * @param options.expiresInSec an explicit lifetime (a session's access
6188
+ * token): wins over `auth.tokenExpiry`, bounded to at least one minute.
6189
+ */
6190
+ signToken(payload, options = {}) {
6191
+ if (options.expiresInSec !== void 0) {
6192
+ const seconds = Math.max(60, Math.floor(options.expiresInSec));
6193
+ return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn: seconds });
6194
+ }
6225
6195
  const configured = this.readConfiguredExpiry();
6226
6196
  const parsed = parseTokenExpiry(configured);
6227
6197
  if (!parsed.ok) {
@@ -6263,42 +6233,14 @@ var AuthManager = class {
6263
6233
  return import_jsonwebtoken.verify(token, this.jwtSecret);
6264
6234
  }
6265
6235
  async hashPassword(password) {
6266
- return hash(password, 10);
6236
+ return hash$1(password, 10);
6267
6237
  }
6268
6238
  async comparePassword(password, hash) {
6269
6239
  return compare(password, hash);
6270
6240
  }
6271
- generateApiKey() {
6272
- const token = crypto$2.randomBytes(32).toString("hex");
6273
- return {
6274
- token,
6275
- hash: crypto$2.createHash("sha256").update(token).digest("hex"),
6276
- prefix: token.slice(0, 8)
6277
- };
6278
- }
6279
- validateApiKey(token, storedHash) {
6280
- return crypto$2.createHash("sha256").update(token).digest("hex") === storedHash;
6281
- }
6282
- /**
6283
- * Create a service token for agent/worker authentication.
6284
- * Used when forking workers or when agents register.
6285
- */
6286
- createServiceToken(opts) {
6287
- const payload = {
6288
- userId: opts.agentId,
6289
- username: opts.agentId,
6290
- isAdmin: true,
6291
- type: "service",
6292
- agentId: opts.agentId,
6293
- allowedProviders: "*",
6294
- allowedDevices: {}
6295
- };
6296
- const expiresIn = opts.expiresIn ?? "24h";
6297
- return import_jsonwebtoken.sign(payload, this.jwtSecret, { expiresIn });
6298
- }
6299
6241
  /**
6300
6242
  * Mint a short-lived HMAC-signed bridge token used by SSO-style auth
6301
- * providers (OIDC, SAML, magic-link, …) to hand the post-callback
6243
+ * providers (OIDC, SAML, …) to hand the post-callback
6302
6244
  * claims to `/api/auth/sso/finish` without trusting unsigned query
6303
6245
  * parameters. The endpoint verifies the token signature with the same
6304
6246
  * `jwtSecret` and only then mints the user session JWT. This closes
@@ -6328,7 +6270,8 @@ var AuthManager = class {
6328
6270
  ...payload.sessionId !== void 0 ? { sessionId: payload.sessionId } : {},
6329
6271
  ...payload.refreshTtl !== void 0 ? { refreshTtl: payload.refreshTtl } : {},
6330
6272
  ...payload.linkIntent !== void 0 ? { linkIntent: payload.linkIntent } : {},
6331
- ...payload.issuer !== void 0 ? { issuer: payload.issuer } : {}
6273
+ ...payload.issuer !== void 0 ? { issuer: payload.issuer } : {},
6274
+ ...payload.browserBinding !== void 0 ? { browserBinding: payload.browserBinding } : {}
6332
6275
  };
6333
6276
  /**
6334
6277
  * `'never'` mints with NO `exp` claim, and the option must be OMITTED to do
@@ -6372,6 +6315,7 @@ var AuthManager = class {
6372
6315
  const sessionId = typeof decoded["sessionId"] === "string" ? decoded["sessionId"] : void 0;
6373
6316
  const linkIntent = typeof decoded["linkIntent"] === "string" ? decoded["linkIntent"] : void 0;
6374
6317
  const issuer = typeof decoded["issuer"] === "string" ? decoded["issuer"] : void 0;
6318
+ const browserBinding = typeof decoded["browserBinding"] === "string" ? decoded["browserBinding"] : void 0;
6375
6319
  const rawRefreshTtl = decoded["refreshTtl"];
6376
6320
  const refreshTtl = rawRefreshTtl === "never" || typeof rawRefreshTtl === "number" && rawRefreshTtl > 0 ? rawRefreshTtl : void 0;
6377
6321
  return {
@@ -6389,7 +6333,8 @@ var AuthManager = class {
6389
6333
  ...sessionId !== void 0 ? { sessionId } : {},
6390
6334
  ...refreshTtl !== void 0 ? { refreshTtl } : {},
6391
6335
  ...linkIntent !== void 0 ? { linkIntent } : {},
6392
- ...issuer !== void 0 ? { issuer } : {}
6336
+ ...issuer !== void 0 ? { issuer } : {},
6337
+ ...browserBinding !== void 0 ? { browserBinding } : {}
6393
6338
  };
6394
6339
  } catch {
6395
6340
  return null;
@@ -6415,10 +6360,43 @@ var AuthManager = class {
6415
6360
  kind: "totp-challenge",
6416
6361
  userId: payload.userId,
6417
6362
  username: payload.username,
6418
- isAdmin: payload.isAdmin
6363
+ isAdmin: payload.isAdmin,
6364
+ ...payload.method !== void 0 ? { method: payload.method } : {}
6419
6365
  }, this.jwtSecret, { expiresIn: ttlSec });
6420
6366
  }
6421
6367
  /**
6368
+ * A short-lived proof that the signed-in user just re-entered their password
6369
+ * or used a passkey. Sensitive self-service actions (linking a sign-in
6370
+ * account, disabling 2FA, removing a passkey, minting an admin token)
6371
+ * require one. `kind: 'reauth'` keeps it from ever being a bearer.
6372
+ */
6373
+ signReauthToken(claims, ttlSec = 300) {
6374
+ return import_jsonwebtoken.sign({
6375
+ kind: "reauth",
6376
+ userId: claims.userId,
6377
+ method: claims.method,
6378
+ ...claims.sessionId !== void 0 ? { sid: claims.sessionId } : {}
6379
+ }, this.jwtSecret, { expiresIn: ttlSec });
6380
+ }
6381
+ /** Decode a re-authentication proof, or `null` (forged, expired, another kind). */
6382
+ verifyReauthToken(token) {
6383
+ try {
6384
+ const decoded = import_jsonwebtoken.verify(token, this.jwtSecret);
6385
+ if (decoded["kind"] !== "reauth") return null;
6386
+ const userId = decoded["userId"];
6387
+ const method = decoded["method"];
6388
+ const sid = decoded["sid"];
6389
+ if (typeof userId !== "string" || method !== "password" && method !== "passkey") return null;
6390
+ return {
6391
+ userId,
6392
+ method,
6393
+ ...typeof sid === "string" ? { sessionId: sid } : {}
6394
+ };
6395
+ } catch {
6396
+ return null;
6397
+ }
6398
+ }
6399
+ /**
6422
6400
  * Verify + decode a TOTP challenge token. Returns `null` for any
6423
6401
  * failure (invalid signature, expired, wrong `kind`, missing
6424
6402
  * claims). The caller MUST also verify the 6-digit code against
@@ -6433,10 +6411,12 @@ var AuthManager = class {
6433
6411
  const username = decoded["username"];
6434
6412
  const isAdmin = decoded["isAdmin"];
6435
6413
  if (typeof userId !== "string" || typeof username !== "string" || typeof isAdmin !== "boolean") return null;
6414
+ const method = decoded["method"];
6436
6415
  return {
6437
6416
  userId,
6438
6417
  username,
6439
- isAdmin
6418
+ isAdmin,
6419
+ ...method === "password" || method === "passkey" || method === "sso" ? { method } : {}
6440
6420
  };
6441
6421
  } catch {
6442
6422
  return null;
@@ -6448,7 +6428,7 @@ var AuthManager = class {
6448
6428
  var TOKENS_COLLECTION = "scoped_tokens";
6449
6429
  var TOKEN_PREFIX = "cst_";
6450
6430
  function parseToken(data) {
6451
- return parseRecord("scoped-token", ScopedTokenSchema, data);
6431
+ return parseRecord$1("scoped-token", ScopedTokenSchema, data);
6452
6432
  }
6453
6433
  /**
6454
6434
  * Manages scoped API tokens with restricted addon/route/capability access.
@@ -6459,11 +6439,11 @@ var ScopedTokenManager = class {
6459
6439
  this.store = store;
6460
6440
  }
6461
6441
  async create(userId, name, scopes, expiresAt) {
6462
- const rawToken = `${TOKEN_PREFIX}${crypto$2.randomBytes(32).toString("hex")}`;
6463
- const tokenHash = crypto$2.createHash("sha256").update(rawToken).digest("hex");
6442
+ const rawToken = `${TOKEN_PREFIX}${crypto$1.randomBytes(32).toString("hex")}`;
6443
+ const tokenHash = crypto$1.createHash("sha256").update(rawToken).digest("hex");
6464
6444
  const tokenPrefix = rawToken.slice(0, 12);
6465
6445
  const record = {
6466
- id: crypto$2.randomUUID(),
6446
+ id: crypto$1.randomUUID(),
6467
6447
  userId,
6468
6448
  name,
6469
6449
  tokenHash,
@@ -6487,7 +6467,7 @@ var ScopedTokenManager = class {
6487
6467
  }
6488
6468
  async validate(rawToken) {
6489
6469
  if (!rawToken.startsWith(TOKEN_PREFIX)) return null;
6490
- const tokenHash = crypto$2.createHash("sha256").update(rawToken).digest("hex");
6470
+ const tokenHash = crypto$1.createHash("sha256").update(rawToken).digest("hex");
6491
6471
  const results = await this.store.query.query({
6492
6472
  collection: TOKENS_COLLECTION,
6493
6473
  filter: { where: { tokenHash } }
@@ -6610,6 +6590,808 @@ var ScopedTokenManager = class {
6610
6590
  }
6611
6591
  };
6612
6592
  //#endregion
6593
+ //#region src/auth/login-session-manager.ts
6594
+ /**
6595
+ * Login sessions — the server-side record behind every session token.
6596
+ *
6597
+ * A session token used to be a self-contained 30-day JWT: nothing on the hub
6598
+ * knew it existed, so logout revoked nothing, a password change left every
6599
+ * other device signed in, and a deleted user's sessions lived until they
6600
+ * expired. Now every login creates a row here and the token carries its id
6601
+ * (`sid`); the hub checks the row on every request (`check`, an in-memory
6602
+ * lookup), so revoking a row ends the session at once.
6603
+ *
6604
+ * Two kinds of client:
6605
+ * - **refreshable** — gets a short access token (default 2 h) and an opaque
6606
+ * refresh token (`csr_…`, stored hashed). `refresh` rotates the refresh
6607
+ * token and slides the session's expiry; presenting a rotated-out refresh
6608
+ * token after a short grace window is treated as theft and revokes the
6609
+ * whole session.
6610
+ * - **legacy** — a client from before refresh tokens: its access token lives
6611
+ * as long as the session, and it renews through the handoff-code pair
6612
+ * (`extend`). Kept for one release so installed viewer apps keep working.
6613
+ *
6614
+ * `legacyCutoff(userId)` covers tokens minted before sessions existed (no
6615
+ * `sid`): a token of that user issued before the cutoff is refused. Revoking
6616
+ * all of a user's sessions moves the cutoff, so a password change or a user
6617
+ * deletion also ends their pre-session tokens.
6618
+ *
6619
+ * Mirror: every row is held in memory (a homelab hub has tens of sessions) and
6620
+ * written through to the store; revocations update the mirror FIRST, so a
6621
+ * store hiccup can delay persistence but never the revocation itself.
6622
+ */
6623
+ /**
6624
+ * @durable class=ledger owner=local-auth
6625
+ * write="one row per login (password, passkey, SSO, handoff, session exchange);
6626
+ * rewritten on every refresh / renewal (expiry slides, refresh hash rotates)
6627
+ * and on revocation"
6628
+ * retention="the daily reap deletes rows expired more than 7 days ago and rows
6629
+ * revoked more than 30 days ago. A session the user keeps refreshing lives as
6630
+ * long as it is used."
6631
+ */
6632
+ var LOGIN_SESSIONS_COLLECTION = "login_sessions";
6633
+ /**
6634
+ * @durable class=config owner=local-auth
6635
+ * write="revoking all of a user's sessions (logout everywhere, password change or
6636
+ * reset, user deletion) sets that user's cutoff to now"
6637
+ * retention="none — one row per user who ever had all sessions revoked; it only
6638
+ * ever matters for tokens minted before sessions existed, and is harmless after."
6639
+ */
6640
+ var LOGIN_SESSION_CUTOFFS_COLLECTION = "login_session_cutoffs";
6641
+ var LOGIN_SESSIONS_COLUMNS = [
6642
+ {
6643
+ name: "id",
6644
+ type: "TEXT",
6645
+ primaryKey: true,
6646
+ notNull: true
6647
+ },
6648
+ {
6649
+ name: "userId",
6650
+ type: "TEXT",
6651
+ notNull: true
6652
+ },
6653
+ {
6654
+ name: "method",
6655
+ type: "TEXT",
6656
+ notNull: true
6657
+ },
6658
+ {
6659
+ name: "refreshable",
6660
+ type: "BOOLEAN",
6661
+ notNull: true
6662
+ },
6663
+ {
6664
+ name: "createdAt",
6665
+ type: "INTEGER",
6666
+ notNull: true
6667
+ },
6668
+ {
6669
+ name: "lastUsedAt",
6670
+ type: "INTEGER",
6671
+ notNull: true
6672
+ },
6673
+ {
6674
+ name: "expiresAt",
6675
+ type: "INTEGER",
6676
+ notNull: true
6677
+ },
6678
+ {
6679
+ name: "revokedAt",
6680
+ type: "INTEGER"
6681
+ },
6682
+ {
6683
+ name: "revokedReason",
6684
+ type: "TEXT"
6685
+ },
6686
+ {
6687
+ name: "refreshHash",
6688
+ type: "TEXT"
6689
+ },
6690
+ {
6691
+ name: "previousRefreshHash",
6692
+ type: "TEXT"
6693
+ },
6694
+ {
6695
+ name: "rotatedAt",
6696
+ type: "INTEGER"
6697
+ },
6698
+ {
6699
+ name: "client",
6700
+ type: "TEXT"
6701
+ },
6702
+ {
6703
+ name: "ip",
6704
+ type: "TEXT"
6705
+ }
6706
+ ];
6707
+ var LOGIN_SESSION_CUTOFFS_COLUMNS = [{
6708
+ name: "userId",
6709
+ type: "TEXT",
6710
+ primaryKey: true,
6711
+ notNull: true
6712
+ }, {
6713
+ name: "notBefore",
6714
+ type: "INTEGER",
6715
+ notNull: true
6716
+ }];
6717
+ var METHODS = [
6718
+ "password",
6719
+ "passkey",
6720
+ "sso",
6721
+ "handoff",
6722
+ "exchange"
6723
+ ];
6724
+ var REFRESH_TOKEN_PREFIX = "csr_";
6725
+ /**
6726
+ * A rotated-out refresh token is still honoured this long: two tabs (or a
6727
+ * retry after a dropped response) may present the same token a moment apart.
6728
+ * After it, the old token is evidence of a copy and ends the session.
6729
+ */
6730
+ var REFRESH_REUSE_GRACE_MS = 3e4;
6731
+ var EXPIRED_RETENTION_MS = 7 * 864e5;
6732
+ var REVOKED_RETENTION_MS = 30 * 864e5;
6733
+ /** Longest client label kept (a User-Agent can be huge). */
6734
+ var MAX_CLIENT_LABEL = 200;
6735
+ function hashToken(token) {
6736
+ return crypto$1.createHash("sha256").update(token).digest("hex");
6737
+ }
6738
+ function str(data, key) {
6739
+ const value = data[key];
6740
+ return typeof value === "string" && value.length > 0 ? value : null;
6741
+ }
6742
+ function num(data, key) {
6743
+ const value = data[key];
6744
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
6745
+ }
6746
+ function parseSession$1(data) {
6747
+ const id = str(data, "id");
6748
+ const userId = str(data, "userId");
6749
+ const createdAt = num(data, "createdAt");
6750
+ const lastUsedAt = num(data, "lastUsedAt");
6751
+ const expiresAt = num(data, "expiresAt");
6752
+ const method = METHODS.find((m) => m === data["method"]);
6753
+ if (id === null || userId === null || createdAt === null || expiresAt === null) return null;
6754
+ return {
6755
+ id,
6756
+ userId,
6757
+ method: method ?? "password",
6758
+ refreshable: data["refreshable"] === true || data["refreshable"] === 1,
6759
+ createdAt,
6760
+ lastUsedAt: lastUsedAt ?? createdAt,
6761
+ expiresAt,
6762
+ revokedAt: num(data, "revokedAt"),
6763
+ revokedReason: str(data, "revokedReason"),
6764
+ refreshHash: str(data, "refreshHash"),
6765
+ previousRefreshHash: str(data, "previousRefreshHash"),
6766
+ rotatedAt: num(data, "rotatedAt"),
6767
+ client: str(data, "client"),
6768
+ ip: str(data, "ip")
6769
+ };
6770
+ }
6771
+ function toLoginSessionSummary(record) {
6772
+ const { refreshHash: _r, previousRefreshHash: _p, rotatedAt: _t, ...summary } = record;
6773
+ return summary;
6774
+ }
6775
+ var LoginSessionManager = class {
6776
+ store;
6777
+ sessions = /* @__PURE__ */ new Map();
6778
+ /** refresh hash (current or previous) → session id */
6779
+ byRefreshHash = /* @__PURE__ */ new Map();
6780
+ cutoffs = /* @__PURE__ */ new Map();
6781
+ now;
6782
+ logger;
6783
+ constructor(store, options = {}) {
6784
+ this.store = store;
6785
+ this.now = options.now ?? Date.now;
6786
+ this.logger = options.logger ?? null;
6787
+ }
6788
+ /** Load every row into the mirror. Call once at init, before serving. */
6789
+ async load() {
6790
+ const rows = await this.store.query.query({ collection: LOGIN_SESSIONS_COLLECTION });
6791
+ for (const row of rows) {
6792
+ const record = parseSession$1(row.data);
6793
+ if (record !== null) this.remember(record);
6794
+ }
6795
+ const cutoffs = await this.store.query.query({ collection: LOGIN_SESSION_CUTOFFS_COLLECTION });
6796
+ for (const row of cutoffs) {
6797
+ const userId = str(row.data, "userId");
6798
+ const notBefore = num(row.data, "notBefore");
6799
+ if (userId !== null && notBefore !== null) this.cutoffs.set(userId, notBefore);
6800
+ }
6801
+ }
6802
+ async create(input) {
6803
+ const at = this.now();
6804
+ const refreshToken = input.refreshable ? this.newRefreshToken() : null;
6805
+ const session = {
6806
+ id: crypto$1.randomUUID(),
6807
+ userId: input.userId,
6808
+ method: input.method,
6809
+ refreshable: input.refreshable,
6810
+ createdAt: at,
6811
+ lastUsedAt: at,
6812
+ expiresAt: at + input.lifetimeMs,
6813
+ revokedAt: null,
6814
+ revokedReason: null,
6815
+ refreshHash: refreshToken === null ? null : hashToken(refreshToken),
6816
+ previousRefreshHash: null,
6817
+ rotatedAt: null,
6818
+ client: input.client === void 0 ? null : input.client.slice(0, MAX_CLIENT_LABEL),
6819
+ ip: input.ip ?? null
6820
+ };
6821
+ await this.store.insert.mutate({
6822
+ collection: LOGIN_SESSIONS_COLLECTION,
6823
+ record: {
6824
+ id: session.id,
6825
+ data: { ...session }
6826
+ }
6827
+ });
6828
+ this.remember(session);
6829
+ return {
6830
+ session,
6831
+ refreshToken
6832
+ };
6833
+ }
6834
+ /** Is `sessionId` a live session right now? Synchronous: the hub asks on every request. */
6835
+ check(sessionId) {
6836
+ const session = this.sessions.get(sessionId);
6837
+ if (session === void 0) return { state: "unknown" };
6838
+ if (session.revokedAt !== null) return { state: "revoked" };
6839
+ if (session.expiresAt <= this.now()) return { state: "expired" };
6840
+ return {
6841
+ state: "active",
6842
+ session
6843
+ };
6844
+ }
6845
+ /** Epoch ms before which a pre-session (no `sid`) token of `userId` is refused; 0 = none. */
6846
+ legacyCutoff(userId) {
6847
+ return this.cutoffs.get(userId) ?? 0;
6848
+ }
6849
+ /** Swap a refresh token for a rotated one, sliding the session's expiry to now + `lifetimeMs`. */
6850
+ async refresh(refreshToken, lifetimeMs) {
6851
+ if (!refreshToken.startsWith("csr_")) return {
6852
+ ok: false,
6853
+ reason: "unknown"
6854
+ };
6855
+ const presented = hashToken(refreshToken);
6856
+ const sessionId = this.byRefreshHash.get(presented);
6857
+ const session = sessionId === void 0 ? void 0 : this.sessions.get(sessionId);
6858
+ if (session === void 0) return {
6859
+ ok: false,
6860
+ reason: "unknown"
6861
+ };
6862
+ if (session.revokedAt !== null) return {
6863
+ ok: false,
6864
+ reason: "revoked"
6865
+ };
6866
+ const at = this.now();
6867
+ if (session.expiresAt <= at) return {
6868
+ ok: false,
6869
+ reason: "expired"
6870
+ };
6871
+ if (presented !== session.refreshHash) {
6872
+ if (!(presented === session.previousRefreshHash && session.rotatedAt !== null && at - session.rotatedAt <= REFRESH_REUSE_GRACE_MS)) {
6873
+ await this.revoke(session.id, "refresh-reuse");
6874
+ this.logger?.warn("A rotated-out refresh token was presented — session revoked", { meta: {
6875
+ sessionId: session.id,
6876
+ userId: session.userId
6877
+ } });
6878
+ return {
6879
+ ok: false,
6880
+ reason: "reused"
6881
+ };
6882
+ }
6883
+ }
6884
+ const next = this.newRefreshToken();
6885
+ const updated = {
6886
+ ...session,
6887
+ lastUsedAt: at,
6888
+ expiresAt: at + lifetimeMs,
6889
+ previousRefreshHash: session.refreshHash,
6890
+ refreshHash: hashToken(next),
6891
+ rotatedAt: at
6892
+ };
6893
+ this.replace(session, updated);
6894
+ await this.persist(updated);
6895
+ return {
6896
+ ok: true,
6897
+ session: updated,
6898
+ refreshToken: next
6899
+ };
6900
+ }
6901
+ /** Renew an active session (legacy client): slide its expiry. `null` when it is not active. */
6902
+ async extend(sessionId, lifetimeMs) {
6903
+ const check = this.check(sessionId);
6904
+ if (check.state !== "active") return null;
6905
+ const at = this.now();
6906
+ const updated = {
6907
+ ...check.session,
6908
+ lastUsedAt: at,
6909
+ expiresAt: at + lifetimeMs
6910
+ };
6911
+ this.replace(check.session, updated);
6912
+ await this.persist(updated);
6913
+ return updated;
6914
+ }
6915
+ /** End one session. `false` when it was unknown or already ended. */
6916
+ async revoke(sessionId, reason) {
6917
+ const session = this.sessions.get(sessionId);
6918
+ if (session === void 0 || session.revokedAt !== null) return false;
6919
+ const updated = {
6920
+ ...session,
6921
+ revokedAt: this.now(),
6922
+ revokedReason: reason
6923
+ };
6924
+ this.replace(session, updated);
6925
+ await this.persist(updated);
6926
+ return true;
6927
+ }
6928
+ /**
6929
+ * End every live session of `userId` except `exceptSessionId`, and move the
6930
+ * user's legacy cutoff to now (pre-session tokens die too). Returns how many
6931
+ * sessions ended.
6932
+ */
6933
+ async revokeAllForUser(userId, options) {
6934
+ const at = this.now();
6935
+ this.cutoffs.set(userId, at);
6936
+ const ended = [];
6937
+ for (const session of this.sessions.values()) {
6938
+ if (session.userId !== userId || session.revokedAt !== null) continue;
6939
+ if (session.id === options.exceptSessionId) continue;
6940
+ const updated = {
6941
+ ...session,
6942
+ revokedAt: at,
6943
+ revokedReason: options.reason
6944
+ };
6945
+ this.replace(session, updated);
6946
+ ended.push(updated);
6947
+ }
6948
+ await this.persistCutoff(userId, at);
6949
+ for (const session of ended) await this.persist(session);
6950
+ return ended.length;
6951
+ }
6952
+ /** `userId`'s live sessions, newest first. */
6953
+ listForUser(userId) {
6954
+ return this.listActive().filter((s) => s.userId === userId);
6955
+ }
6956
+ /** Every live session, newest first. */
6957
+ listActive() {
6958
+ const at = this.now();
6959
+ return [...this.sessions.values()].filter((s) => s.revokedAt === null && s.expiresAt > at).toSorted((a, b) => b.createdAt - a.createdAt).map(toLoginSessionSummary);
6960
+ }
6961
+ /** Delete rows expired more than 7 days ago or revoked more than 30. Returns how many. */
6962
+ async reap() {
6963
+ const at = this.now();
6964
+ 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);
6965
+ for (const session of dead) {
6966
+ this.forget(session);
6967
+ await this.store.delete.mutate({
6968
+ collection: LOGIN_SESSIONS_COLLECTION,
6969
+ key: session.id
6970
+ });
6971
+ }
6972
+ return dead.length;
6973
+ }
6974
+ newRefreshToken() {
6975
+ return `${REFRESH_TOKEN_PREFIX}${crypto$1.randomBytes(32).toString("base64url")}`;
6976
+ }
6977
+ remember(session) {
6978
+ this.sessions.set(session.id, session);
6979
+ if (session.refreshHash !== null) this.byRefreshHash.set(session.refreshHash, session.id);
6980
+ if (session.previousRefreshHash !== null) this.byRefreshHash.set(session.previousRefreshHash, session.id);
6981
+ }
6982
+ forget(session) {
6983
+ this.sessions.delete(session.id);
6984
+ if (session.refreshHash !== null) this.byRefreshHash.delete(session.refreshHash);
6985
+ if (session.previousRefreshHash !== null) this.byRefreshHash.delete(session.previousRefreshHash);
6986
+ }
6987
+ replace(previous, next) {
6988
+ this.forget(previous);
6989
+ this.remember(next);
6990
+ }
6991
+ async persist(session) {
6992
+ try {
6993
+ await this.store.update.mutate({
6994
+ collection: LOGIN_SESSIONS_COLLECTION,
6995
+ id: session.id,
6996
+ data: { ...session }
6997
+ });
6998
+ } catch (error) {
6999
+ this.logger?.error("Login session write failed — the change holds until restart", { meta: {
7000
+ sessionId: session.id,
7001
+ error: error instanceof Error ? error.message : String(error)
7002
+ } });
7003
+ }
7004
+ }
7005
+ async persistCutoff(userId, notBefore) {
7006
+ try {
7007
+ await this.store.delete.mutate({
7008
+ collection: LOGIN_SESSION_CUTOFFS_COLLECTION,
7009
+ key: userId
7010
+ });
7011
+ await this.store.insert.mutate({
7012
+ collection: LOGIN_SESSION_CUTOFFS_COLLECTION,
7013
+ record: {
7014
+ id: userId,
7015
+ data: {
7016
+ userId,
7017
+ notBefore
7018
+ }
7019
+ }
7020
+ });
7021
+ } catch (error) {
7022
+ this.logger?.error("Login session cutoff write failed — it holds until restart", { meta: {
7023
+ userId,
7024
+ error: error instanceof Error ? error.message : String(error)
7025
+ } });
7026
+ }
7027
+ }
7028
+ };
7029
+ //#endregion
7030
+ //#region src/auth/admin-token-manager.ts
7031
+ /**
7032
+ * Admin tokens (`csa_…`) — the one credential an ops script can hold that
7033
+ * acts as an administrator, minted deliberately and kept on a short leash.
7034
+ *
7035
+ * Scoped tokens (`cst_`) are never admin, and a script that needs admin (the
7036
+ * keychain migration, a release's `applyServerUpdate`) otherwise needs a human
7037
+ * login. This is that credential, with the rules the operator set:
7038
+ *
7039
+ * - created only by an admin, from the UI, after re-authentication (the hub
7040
+ * router enforces both);
7041
+ * - expiry MANDATORY, at most 90 days;
7042
+ * - shown once — only a SHA-256 hash is stored, like `cst_`;
7043
+ * - every use audit-logged (the hub, per request);
7044
+ * - revocable, and dead the moment its creator is deleted or loses admin;
7045
+ * - NOT a session: an opaque token, so it survives a signing-key change.
7046
+ */
7047
+ /**
7048
+ * @durable class=config owner=local-auth
7049
+ * write="an admin creates a token (after re-authentication); `lastUsedAt` is
7050
+ * rewritten at most once a minute while it is used; `revoke` stamps revokedAt"
7051
+ * retention="the daily reap deletes rows expired or revoked more than 30 days ago"
7052
+ */
7053
+ var ADMIN_TOKENS_COLLECTION = "admin_tokens";
7054
+ var ADMIN_TOKENS_COLUMNS = [
7055
+ {
7056
+ name: "id",
7057
+ type: "TEXT",
7058
+ primaryKey: true,
7059
+ notNull: true
7060
+ },
7061
+ {
7062
+ name: "createdBy",
7063
+ type: "TEXT",
7064
+ notNull: true
7065
+ },
7066
+ {
7067
+ name: "name",
7068
+ type: "TEXT",
7069
+ notNull: true
7070
+ },
7071
+ {
7072
+ name: "tokenHash",
7073
+ type: "TEXT",
7074
+ notNull: true,
7075
+ unique: true
7076
+ },
7077
+ {
7078
+ name: "tokenPrefix",
7079
+ type: "TEXT",
7080
+ notNull: true
7081
+ },
7082
+ {
7083
+ name: "createdAt",
7084
+ type: "INTEGER",
7085
+ notNull: true
7086
+ },
7087
+ {
7088
+ name: "expiresAt",
7089
+ type: "INTEGER",
7090
+ notNull: true
7091
+ },
7092
+ {
7093
+ name: "lastUsedAt",
7094
+ type: "INTEGER"
7095
+ },
7096
+ {
7097
+ name: "revokedAt",
7098
+ type: "INTEGER"
7099
+ }
7100
+ ];
7101
+ var ADMIN_TOKEN_PREFIX = "csa_";
7102
+ var ADMIN_TOKEN_MAX_LIFETIME_MS = 90 * 864e5;
7103
+ var DEAD_RETENTION_MS = 30 * 864e5;
7104
+ var TOUCH_EVERY_MS = 6e4;
7105
+ function hash(token) {
7106
+ return crypto$1.createHash("sha256").update(token).digest("hex");
7107
+ }
7108
+ function parse(data) {
7109
+ const { id, createdBy, name, tokenHash, tokenPrefix, createdAt, expiresAt } = data;
7110
+ if (typeof id !== "string" || typeof createdBy !== "string" || typeof name !== "string" || typeof tokenHash !== "string" || typeof tokenPrefix !== "string" || typeof createdAt !== "number" || typeof expiresAt !== "number") return null;
7111
+ return {
7112
+ id,
7113
+ createdBy,
7114
+ name,
7115
+ tokenHash,
7116
+ tokenPrefix,
7117
+ createdAt,
7118
+ expiresAt,
7119
+ lastUsedAt: typeof data["lastUsedAt"] === "number" ? data["lastUsedAt"] : null,
7120
+ revokedAt: typeof data["revokedAt"] === "number" ? data["revokedAt"] : null
7121
+ };
7122
+ }
7123
+ function summary(stored) {
7124
+ const { tokenHash: _hash, ...record } = stored;
7125
+ return record;
7126
+ }
7127
+ var AdminTokenManager = class {
7128
+ store;
7129
+ now;
7130
+ constructor(store, now = Date.now) {
7131
+ this.store = store;
7132
+ this.now = now;
7133
+ }
7134
+ /** Mint a token for `createdBy`. Refuses an expiry in the past or past 90 days. */
7135
+ async create(input) {
7136
+ const at = this.now();
7137
+ if (!(input.expiresAt > at) || input.expiresAt > at + 7776e6) throw new Error("An admin token must expire within 90 days");
7138
+ const name = input.name.trim().slice(0, 80);
7139
+ if (name.length === 0) throw new Error("An admin token needs a name");
7140
+ const token = `${ADMIN_TOKEN_PREFIX}${crypto$1.randomBytes(32).toString("base64url")}`;
7141
+ const stored = {
7142
+ id: crypto$1.randomUUID(),
7143
+ createdBy: input.createdBy,
7144
+ name,
7145
+ tokenHash: hash(token),
7146
+ tokenPrefix: token.slice(0, 12),
7147
+ createdAt: at,
7148
+ expiresAt: input.expiresAt,
7149
+ lastUsedAt: null,
7150
+ revokedAt: null
7151
+ };
7152
+ await this.store.insert.mutate({
7153
+ collection: ADMIN_TOKENS_COLLECTION,
7154
+ record: {
7155
+ id: stored.id,
7156
+ data: { ...stored }
7157
+ }
7158
+ });
7159
+ return {
7160
+ token,
7161
+ record: summary(stored)
7162
+ };
7163
+ }
7164
+ /** The live record behind `token`, or `null` (unknown, revoked, expired, not a `csa_`). */
7165
+ async validate(token) {
7166
+ if (!token.startsWith("csa_")) return null;
7167
+ const first = (await this.store.query.query({
7168
+ collection: ADMIN_TOKENS_COLLECTION,
7169
+ filter: { where: { tokenHash: hash(token) } }
7170
+ }))[0];
7171
+ const stored = first === void 0 ? null : parse(first.data);
7172
+ if (stored === null || stored.revokedAt !== null) return null;
7173
+ const at = this.now();
7174
+ if (stored.expiresAt <= at) return null;
7175
+ if (stored.lastUsedAt === null || at - stored.lastUsedAt > TOUCH_EVERY_MS) this.store.update.mutate({
7176
+ collection: ADMIN_TOKENS_COLLECTION,
7177
+ id: stored.id,
7178
+ data: {
7179
+ ...stored,
7180
+ lastUsedAt: at
7181
+ }
7182
+ }).catch(() => {});
7183
+ return summary(stored);
7184
+ }
7185
+ /** Every token not yet reaped, newest first (no hashes). */
7186
+ async list() {
7187
+ 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);
7188
+ }
7189
+ /** Revoke one token. `false` when unknown or already revoked. */
7190
+ async revoke(id) {
7191
+ const first = (await this.store.query.query({
7192
+ collection: ADMIN_TOKENS_COLLECTION,
7193
+ filter: { where: { id } }
7194
+ }))[0];
7195
+ const stored = first === void 0 ? null : parse(first.data);
7196
+ if (stored === null || stored.revokedAt !== null) return false;
7197
+ await this.store.update.mutate({
7198
+ collection: ADMIN_TOKENS_COLLECTION,
7199
+ id,
7200
+ data: {
7201
+ ...stored,
7202
+ revokedAt: this.now()
7203
+ }
7204
+ });
7205
+ return true;
7206
+ }
7207
+ /** Revoke every live token `userId` created (the user was deleted). */
7208
+ async revokeAllCreatedBy(userId) {
7209
+ let revoked = 0;
7210
+ for (const token of await this.list()) if (token.createdBy === userId && token.revokedAt === null) {
7211
+ if (await this.revoke(token.id)) revoked++;
7212
+ }
7213
+ return revoked;
7214
+ }
7215
+ /** Delete rows expired or revoked more than 30 days ago. */
7216
+ async reap() {
7217
+ const horizon = this.now() - DEAD_RETENTION_MS;
7218
+ let deleted = 0;
7219
+ for (const token of await this.list()) if ((token.revokedAt ?? token.expiresAt) < horizon) {
7220
+ await this.store.delete.mutate({
7221
+ collection: ADMIN_TOKENS_COLLECTION,
7222
+ key: token.id
7223
+ });
7224
+ deleted++;
7225
+ }
7226
+ return deleted;
7227
+ }
7228
+ };
7229
+ //#endregion
7230
+ //#region src/auth/auth-audit-log.ts
7231
+ /**
7232
+ * The authentication audit trail: what an operator must be able to answer
7233
+ * after the fact — "who skipped 2FA from the LAN, and from where?", "which
7234
+ * admin token ran this, and when?".
7235
+ *
7236
+ * One row per event, kept 90 days. Writing never throws: an audit row that
7237
+ * cannot be stored is logged loudly instead, and never blocks the action
7238
+ * (a login must not fail because the audit table is busy).
7239
+ */
7240
+ /**
7241
+ * @durable class=ledger owner=local-auth
7242
+ * write="one row per audited auth event: a second factor skipped from a trusted
7243
+ * network, an admin token created / used / revoked"
7244
+ * retention="the daily reap deletes rows older than 90 days"
7245
+ */
7246
+ var AUTH_AUDIT_COLLECTION = "auth_audit";
7247
+ var AUTH_AUDIT_COLUMNS = [
7248
+ {
7249
+ name: "id",
7250
+ type: "TEXT",
7251
+ primaryKey: true,
7252
+ notNull: true
7253
+ },
7254
+ {
7255
+ name: "at",
7256
+ type: "INTEGER",
7257
+ notNull: true
7258
+ },
7259
+ {
7260
+ name: "action",
7261
+ type: "TEXT",
7262
+ notNull: true
7263
+ },
7264
+ {
7265
+ name: "userId",
7266
+ type: "TEXT"
7267
+ },
7268
+ {
7269
+ name: "ip",
7270
+ type: "TEXT"
7271
+ },
7272
+ {
7273
+ name: "detail",
7274
+ type: "TEXT"
7275
+ }
7276
+ ];
7277
+ var AUTH_AUDIT_ACTIONS = [
7278
+ "second-factor-skipped",
7279
+ "admin-token-created",
7280
+ "admin-token-used",
7281
+ "admin-token-revoked"
7282
+ ];
7283
+ var RETENTION_MS = 90 * 864e5;
7284
+ var MAX_LIST = 500;
7285
+ function parseRecord(data) {
7286
+ const id = data["id"];
7287
+ const at = data["at"];
7288
+ const action = data["action"];
7289
+ const known = AUTH_AUDIT_ACTIONS.find((a) => a === action);
7290
+ if (typeof id !== "string" || typeof at !== "number" || known === void 0) return null;
7291
+ let detail;
7292
+ if (typeof data["detail"] === "string") try {
7293
+ const parsed = JSON.parse(data["detail"]);
7294
+ 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"));
7295
+ } catch {
7296
+ detail = void 0;
7297
+ }
7298
+ return {
7299
+ id,
7300
+ at,
7301
+ action: known,
7302
+ ...typeof data["userId"] === "string" ? { userId: data["userId"] } : {},
7303
+ ...typeof data["ip"] === "string" ? { ip: data["ip"] } : {},
7304
+ ...detail !== void 0 ? { detail } : {}
7305
+ };
7306
+ }
7307
+ var AuthAuditLog = class {
7308
+ store;
7309
+ logger;
7310
+ now;
7311
+ constructor(store, logger = null, now = Date.now) {
7312
+ this.store = store;
7313
+ this.logger = logger;
7314
+ this.now = now;
7315
+ }
7316
+ /** Record `event`. Never throws. */
7317
+ async record(event) {
7318
+ const row = {
7319
+ id: crypto$1.randomUUID(),
7320
+ at: this.now(),
7321
+ action: event.action,
7322
+ userId: event.userId ?? null,
7323
+ ip: event.ip ?? null,
7324
+ detail: event.detail === void 0 ? null : JSON.stringify(event.detail)
7325
+ };
7326
+ this.logger?.info(`auth audit: ${event.action}`, { meta: {
7327
+ userId: event.userId,
7328
+ ip: event.ip,
7329
+ ...event.detail
7330
+ } });
7331
+ try {
7332
+ await this.store.insert.mutate({
7333
+ collection: AUTH_AUDIT_COLLECTION,
7334
+ record: {
7335
+ id: row.id,
7336
+ data: row
7337
+ }
7338
+ });
7339
+ } catch (error) {
7340
+ this.logger?.error("auth audit row not written", { meta: {
7341
+ action: event.action,
7342
+ error: error instanceof Error ? error.message : String(error)
7343
+ } });
7344
+ }
7345
+ }
7346
+ /** Newest first, at most `limit` (≤ 500), optionally one user's. */
7347
+ async list(input = {}) {
7348
+ const rows = await this.store.query.query({
7349
+ collection: AUTH_AUDIT_COLLECTION,
7350
+ ...input.userId !== void 0 ? { filter: { where: { userId: input.userId } } } : {}
7351
+ });
7352
+ const limit = Math.min(MAX_LIST, Math.max(1, input.limit ?? 100));
7353
+ return rows.map((row) => parseRecord(row.data)).filter((r) => r !== null).toSorted((a, b) => b.at - a.at).slice(0, limit);
7354
+ }
7355
+ /** Delete rows older than 90 days. Returns how many. */
7356
+ async reap() {
7357
+ const horizon = this.now() - RETENTION_MS;
7358
+ const rows = await this.store.query.query({ collection: AUTH_AUDIT_COLLECTION });
7359
+ let deleted = 0;
7360
+ for (const row of rows) {
7361
+ const at = row.data["at"];
7362
+ if (typeof at === "number" && at < horizon) {
7363
+ await this.store.delete.mutate({
7364
+ collection: AUTH_AUDIT_COLLECTION,
7365
+ key: String(row.id)
7366
+ });
7367
+ deleted++;
7368
+ }
7369
+ }
7370
+ return deleted;
7371
+ }
7372
+ };
7373
+ //#endregion
7374
+ //#region src/auth/user-management-internal.ts
7375
+ /** Process-global, so every bundled copy of this module agrees on the key. */
7376
+ var USER_MANAGEMENT_INTERNAL = Symbol.for("camstack.user-management.internal");
7377
+ /** Attach `internal` to `provider` (returns the same object, now carrying it). */
7378
+ function attachUserManagementInternal(provider, internal) {
7379
+ Object.defineProperty(provider, USER_MANAGEMENT_INTERNAL, {
7380
+ value: internal,
7381
+ enumerable: false,
7382
+ writable: false,
7383
+ configurable: false
7384
+ });
7385
+ return provider;
7386
+ }
7387
+ /** The hub-internal surface of a `user-management` provider, or `null` when it carries none. */
7388
+ function getUserManagementInternal(provider) {
7389
+ if (provider === null || typeof provider !== "object") return null;
7390
+ const internal = Reflect.get(provider, USER_MANAGEMENT_INTERNAL);
7391
+ if (internal === null || typeof internal !== "object") return null;
7392
+ return internal;
7393
+ }
7394
+ //#endregion
6613
7395
  //#region src/auth/user-manager.ts
6614
7396
  var USERS_COLLECTION$1 = "users";
6615
7397
  /**
@@ -6621,7 +7403,7 @@ var USERS_COLLECTION$1 = "users";
6621
7403
  var FIRST_BOOT_ADMIN_USERNAME = "admin";
6622
7404
  var FIRST_BOOT_ADMIN_PASSWORD = "changeme";
6623
7405
  function parseUser(data) {
6624
- return parseRecord("user", UserRecordSchema, data);
7406
+ return parseRecord$1("user", UserRecordSchema, data);
6625
7407
  }
6626
7408
  var UserManager = class {
6627
7409
  storageAccess;
@@ -6638,7 +7420,7 @@ var UserManager = class {
6638
7420
  const passwordHash = await this.auth.hashPassword(input.password);
6639
7421
  const now = Date.now();
6640
7422
  const record = {
6641
- id: crypto$2.randomUUID(),
7423
+ id: crypto$1.randomUUID(),
6642
7424
  username: input.username,
6643
7425
  passwordHash,
6644
7426
  isAdmin: input.isAdmin ?? false,
@@ -7195,6 +7977,34 @@ async function declareAuthSchema(store) {
7195
7977
  columns: ["userId"]
7196
7978
  }]
7197
7979
  });
7980
+ await store.declareCollection.mutate({
7981
+ collection: LOGIN_SESSIONS_COLLECTION,
7982
+ columns: LOGIN_SESSIONS_COLUMNS,
7983
+ indexes: [{
7984
+ name: "idx_login_sessions_user_id",
7985
+ columns: ["userId"]
7986
+ }]
7987
+ });
7988
+ await store.declareCollection.mutate({
7989
+ collection: LOGIN_SESSION_CUTOFFS_COLLECTION,
7990
+ columns: LOGIN_SESSION_CUTOFFS_COLUMNS
7991
+ });
7992
+ await store.declareCollection.mutate({
7993
+ collection: ADMIN_TOKENS_COLLECTION,
7994
+ columns: ADMIN_TOKENS_COLUMNS,
7995
+ indexes: [{
7996
+ name: "idx_admin_tokens_created_by",
7997
+ columns: ["createdBy"]
7998
+ }]
7999
+ });
8000
+ await store.declareCollection.mutate({
8001
+ collection: AUTH_AUDIT_COLLECTION,
8002
+ columns: AUTH_AUDIT_COLUMNS,
8003
+ indexes: [{
8004
+ name: "idx_auth_audit_user_id",
8005
+ columns: ["userId"]
8006
+ }]
8007
+ });
7198
8008
  }
7199
8009
  //#endregion
7200
8010
  //#region src/auth/external-identity-manager.ts
@@ -9915,7 +10725,7 @@ var OauthSessionManager = class {
9915
10725
  async create(input) {
9916
10726
  const now = Date.now();
9917
10727
  const session = {
9918
- id: crypto$2.randomUUID(),
10728
+ id: crypto$1.randomUUID(),
9919
10729
  userId: input.userId,
9920
10730
  username: input.username,
9921
10731
  integrationId: input.integrationId,
@@ -10068,6 +10878,9 @@ var LocalAuthAddon = class extends BaseAddon {
10068
10878
  totpManager = null;
10069
10879
  externalIdentities = null;
10070
10880
  signingKeyKeeper = null;
10881
+ loginSessions = null;
10882
+ adminTokens = null;
10883
+ auditLog = null;
10071
10884
  constructor() {
10072
10885
  super({});
10073
10886
  }
@@ -10088,11 +10901,15 @@ var LocalAuthAddon = class extends BaseAddon {
10088
10901
  return store;
10089
10902
  } };
10090
10903
  this.userManager = new UserManager(storageAccess, this.authManager);
10091
- this.apiKeyManager = new ApiKeyManager(storageAccess, this.authManager);
10904
+ this.apiKeyManager = new ApiKeyManager(storageAccess);
10092
10905
  this.scopedTokenManager = new ScopedTokenManager(store);
10093
10906
  this.oauthSessionManager = new OauthSessionManager(store);
10094
10907
  this.totpManager = new TotpManager(store);
10095
10908
  this.externalIdentities = new ExternalIdentityManager(store);
10909
+ this.loginSessions = new LoginSessionManager(store, { logger: this.ctx.logger });
10910
+ await this.loginSessions.load();
10911
+ this.adminTokens = new AdminTokenManager(store);
10912
+ this.auditLog = new AuthAuditLog(store, this.ctx.logger);
10096
10913
  try {
10097
10914
  await reconcileAdminAccountAtBoot({
10098
10915
  users: this.userManager,
@@ -10185,12 +11002,13 @@ var LocalAuthAddon = class extends BaseAddon {
10185
11002
  deleteUser: async (input) => {
10186
11003
  if (!this.userManager) throw new Error("User management not available");
10187
11004
  await this.userManager.delete(input.id);
10188
- await this.externalIdentities?.removeAllForUser(input.id);
11005
+ await this.endAllAccess(input.id);
10189
11006
  return { success: true };
10190
11007
  },
10191
11008
  resetPassword: async (input) => {
10192
11009
  if (!this.userManager) throw new Error("User management not available");
10193
11010
  await this.userManager.resetPassword(input.id, input.newPassword);
11011
+ await this.loginSessions?.revokeAllForUser(input.id, { reason: "password-reset" });
10194
11012
  return { success: true };
10195
11013
  },
10196
11014
  setUserScopes: async (input) => {
@@ -10212,27 +11030,11 @@ var LocalAuthAddon = class extends BaseAddon {
10212
11030
  if (!this.apiKeyManager) return [];
10213
11031
  return this.apiKeyManager.listAll();
10214
11032
  },
10215
- createApiKey: async (input) => {
10216
- if (!this.apiKeyManager) throw new Error("API key management not available");
10217
- const { record, token } = await this.apiKeyManager.create(input);
10218
- const { tokenHash: _, ...summary } = record;
10219
- return {
10220
- token,
10221
- record: summary
10222
- };
10223
- },
10224
11033
  revokeApiKey: async (input) => {
10225
11034
  if (!this.apiKeyManager) throw new Error("API key management not available");
10226
11035
  await this.apiKeyManager.revoke(input.id);
10227
11036
  return { success: true };
10228
11037
  },
10229
- validateApiKey: async (input) => {
10230
- if (!this.apiKeyManager) return null;
10231
- const record = await this.apiKeyManager.validateToken(input.token);
10232
- if (!record) return null;
10233
- const { tokenHash: _, ...summary } = record;
10234
- return summary;
10235
- },
10236
11038
  createScopedToken: async (input) => {
10237
11039
  if (!this.scopedTokenManager) throw new Error("Scoped token management not available");
10238
11040
  const { token, record } = await this.scopedTokenManager.create(input.userId, input.name, input.scopes, input.expiresAt);
@@ -10247,8 +11049,10 @@ var LocalAuthAddon = class extends BaseAddon {
10247
11049
  return { success: true };
10248
11050
  },
10249
11051
  validateScopedToken: async (input) => {
10250
- if (!this.scopedTokenManager) return null;
10251
- return this.scopedTokenManager.validate(input.token);
11052
+ if (!this.scopedTokenManager || !this.userManager) return null;
11053
+ const record = await this.scopedTokenManager.validate(input.token);
11054
+ if (record === null) return null;
11055
+ return await this.userManager.findById(record.userId) === null ? null : record;
10252
11056
  },
10253
11057
  listScopedTokens: async (input) => {
10254
11058
  if (!this.scopedTokenManager) return [];
@@ -10291,12 +11095,6 @@ var LocalAuthAddon = class extends BaseAddon {
10291
11095
  issuer: link.issuer
10292
11096
  };
10293
11097
  },
10294
- linkExternalIdentity: async (input) => {
10295
- if (!this.externalIdentities || !this.userManager) throw new Error("External sign-in linking not available");
10296
- if (!await this.userManager.findById(input.userId)) throw new Error(`User not found: ${input.userId}`);
10297
- await this.externalIdentities.link(input);
10298
- return { success: true };
10299
- },
10300
11098
  unlinkExternalIdentity: async (input) => {
10301
11099
  if (!this.externalIdentities) return { success: false };
10302
11100
  return { success: await this.externalIdentities.unlink(input) };
@@ -10334,6 +11132,7 @@ var LocalAuthAddon = class extends BaseAddon {
10334
11132
  }
10335
11133
  };
10336
11134
  this.ctx.logger.info("registered auth-provider + user-management capabilities");
11135
+ attachUserManagementInternal(userMgmt, this.buildInternal());
10337
11136
  return [{
10338
11137
  capability: authProviderCapability,
10339
11138
  provider: authProvider
@@ -10343,6 +11142,93 @@ var LocalAuthAddon = class extends BaseAddon {
10343
11142
  }];
10344
11143
  }
10345
11144
  /**
11145
+ * The hub-internal surface (`user-management-internal.ts`): operations no
11146
+ * wire may reach. Called only by hub code holding this provider object.
11147
+ */
11148
+ buildInternal() {
11149
+ const sessions = () => {
11150
+ if (this.loginSessions === null) throw new Error("Login sessions not available");
11151
+ return this.loginSessions;
11152
+ };
11153
+ return {
11154
+ sessions: {
11155
+ create: (input) => sessions().create(input),
11156
+ refresh: (token, lifetimeMs) => sessions().refresh(token, lifetimeMs),
11157
+ extend: (id, lifetimeMs) => sessions().extend(id, lifetimeMs),
11158
+ check: (id) => sessions().check(id),
11159
+ legacyCutoff: (userId) => sessions().legacyCutoff(userId),
11160
+ revoke: (id, reason) => sessions().revoke(id, reason),
11161
+ revokeAllForUser: (userId, options) => sessions().revokeAllForUser(userId, options),
11162
+ listForUser: (userId) => sessions().listForUser(userId),
11163
+ listActive: () => sessions().listActive()
11164
+ },
11165
+ adminTokens: {
11166
+ create: async (input) => {
11167
+ if (this.adminTokens === null) throw new Error("Admin tokens not available");
11168
+ return this.adminTokens.create(input);
11169
+ },
11170
+ validate: async (token) => {
11171
+ if (this.adminTokens === null || this.userManager === null) return null;
11172
+ const record = await this.adminTokens.validate(token);
11173
+ if (record === null) return null;
11174
+ const creator = await this.userManager.findById(record.createdBy);
11175
+ return creator !== null && creator.isAdmin ? record : null;
11176
+ },
11177
+ list: async () => this.adminTokens === null ? [] : this.adminTokens.list(),
11178
+ revoke: async (id) => this.adminTokens === null ? false : this.adminTokens.revoke(id)
11179
+ },
11180
+ audit: {
11181
+ record: async (event) => {
11182
+ await this.auditLog?.record(event);
11183
+ },
11184
+ list: async (input) => this.auditLog === null ? [] : this.auditLog.list(input)
11185
+ },
11186
+ setPassword: async (input) => {
11187
+ if (!this.userManager) throw new Error("User management not available");
11188
+ await this.userManager.resetPassword(input.userId, input.newPassword);
11189
+ await sessions().revokeAllForUser(input.userId, {
11190
+ ...input.keepSessionId !== void 0 ? { exceptSessionId: input.keepSessionId } : {},
11191
+ reason: "password-changed"
11192
+ });
11193
+ },
11194
+ linkExternalIdentity: async (input) => {
11195
+ if (!this.externalIdentities || !this.userManager) throw new Error("External sign-in linking not available");
11196
+ if (!await this.userManager.findById(input.userId)) throw new Error(`User not found: ${input.userId}`);
11197
+ await this.externalIdentities.link(input);
11198
+ return { success: true };
11199
+ }
11200
+ };
11201
+ }
11202
+ /**
11203
+ * End every way `userId` could still get in: login sessions (and the
11204
+ * pre-session tokens, via the cutoff), scoped tokens, OAuth account links,
11205
+ * the TOTP row and the external sign-in links. Each step is attempted even
11206
+ * if an earlier one fails, and a failure is logged — a half-cleaned user is
11207
+ * worse than a loud one.
11208
+ */
11209
+ async endAllAccess(userId) {
11210
+ const steps = [
11211
+ ["sessions", async () => this.loginSessions?.revokeAllForUser(userId, { reason: "user-deleted" })],
11212
+ ["scoped tokens", async () => {
11213
+ for (const token of await this.scopedTokenManager?.listForUser(userId) ?? []) await this.scopedTokenManager?.revoke(token.id);
11214
+ }],
11215
+ ["oauth sessions", async () => {
11216
+ for (const session of await this.oauthSessionManager?.list() ?? []) if (session.userId === userId && session.revokedAt === null) await this.oauthSessionManager?.markRevoked(session.id);
11217
+ }],
11218
+ ["admin tokens", async () => this.adminTokens?.revokeAllCreatedBy(userId)],
11219
+ ["totp", async () => this.totpManager?.disable(userId)],
11220
+ ["sign-in links", async () => this.externalIdentities?.removeAllForUser(userId)]
11221
+ ];
11222
+ for (const [what, step] of steps) try {
11223
+ await step();
11224
+ } catch (err) {
11225
+ this.ctx.logger.error(`deleting a user: could not remove its ${what}`, { meta: {
11226
+ userId,
11227
+ error: err instanceof Error ? err.message : String(err)
11228
+ } });
11229
+ }
11230
+ }
11231
+ /**
10346
11232
  * Drop auth rows that are dead but were never removed: scoped tokens past
10347
11233
  * their expiry, and OAuth sessions revoked longer ago than
10348
11234
  * {@link OAUTH_REVOKED_RETENTION_MS}.
@@ -10356,9 +11242,13 @@ var LocalAuthAddon = class extends BaseAddon {
10356
11242
  try {
10357
11243
  const tokens = await this.scopedTokenManager?.reapExpired() ?? 0;
10358
11244
  const sessions = await this.oauthSessionManager?.reapRevoked(Date.now() - OAUTH_REVOKED_RETENTION_MS) ?? 0;
10359
- if (tokens > 0 || sessions > 0) this.ctx.logger.info("auth retention swept", { meta: {
11245
+ const logins = await this.loginSessions?.reap() ?? 0;
11246
+ await this.adminTokens?.reap();
11247
+ await this.auditLog?.reap();
11248
+ if (tokens > 0 || sessions > 0 || logins > 0) this.ctx.logger.info("auth retention swept", { meta: {
10360
11249
  expiredTokens: tokens,
10361
- revokedSessions: sessions
11250
+ revokedSessions: sessions,
11251
+ deadLoginSessions: logins
10362
11252
  } });
10363
11253
  } catch (err) {
10364
11254
  this.ctx.logger.warn("auth retention sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
@@ -10377,7 +11267,10 @@ var LocalAuthAddon = class extends BaseAddon {
10377
11267
  this.scopedTokenManager = null;
10378
11268
  this.oauthSessionManager = null;
10379
11269
  this.externalIdentities = null;
11270
+ this.loginSessions = null;
11271
+ this.adminTokens = null;
11272
+ this.auditLog = null;
10380
11273
  }
10381
11274
  };
10382
11275
  //#endregion
10383
- export { LocalAuthAddon, LocalAuthAddon as default, AuthManager as a, require_ms as c, ScopedTokenManager as i, require_safe_buffer as l, FIRST_BOOT_ADMIN_USERNAME as n, SettingsReadUnavailableError as o, UserManager as r, isSettingsReadUnavailable as s, FIRST_BOOT_ADMIN_PASSWORD as t, ApiKeyManager as u };
11276
+ export { LocalAuthAddon, LocalAuthAddon as default, isSettingsReadUnavailable as _, attachUserManagementInternal as a, ApiKeyManager as b, ADMIN_TOKEN_MAX_LIFETIME_MS as c, LoginSessionManager as d, REFRESH_TOKEN_PREFIX as f, SettingsReadUnavailableError as g, parseTokenExpiry as h, USER_MANAGEMENT_INTERNAL as i, ADMIN_TOKEN_PREFIX as l, AuthManager as m, FIRST_BOOT_ADMIN_USERNAME as n, getUserManagementInternal as o, ScopedTokenManager as p, UserManager as r, AuthAuditLog as s, FIRST_BOOT_ADMIN_PASSWORD as t, AdminTokenManager as u, require_ms as v, require_safe_buffer as y };