@camstack/core 0.1.33 → 0.1.34

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 (34) hide show
  1. package/dist/auth/api-key-manager.d.ts +2 -2
  2. package/dist/auth/api-key-manager.d.ts.map +1 -1
  3. package/dist/auth/auth-manager.d.ts +62 -3
  4. package/dist/auth/auth-manager.d.ts.map +1 -1
  5. package/dist/auth/totp-manager.d.ts +53 -0
  6. package/dist/auth/totp-manager.d.ts.map +1 -0
  7. package/dist/auth/user-manager.d.ts +3 -3
  8. package/dist/auth/user-manager.d.ts.map +1 -1
  9. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.d.ts.map +1 -1
  10. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js +29 -9
  11. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.js.map +1 -1
  12. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs +29 -9
  13. package/dist/builtins/auth-orchestrator/auth-orchestrator.addon.mjs.map +1 -1
  14. package/dist/builtins/local-auth/auth-schema.d.ts +14 -0
  15. package/dist/builtins/local-auth/auth-schema.d.ts.map +1 -1
  16. package/dist/builtins/local-auth/local-auth.addon.d.ts +1 -0
  17. package/dist/builtins/local-auth/local-auth.addon.d.ts.map +1 -1
  18. package/dist/builtins/local-auth/local-auth.addon.js +1011 -22
  19. package/dist/builtins/local-auth/local-auth.addon.js.map +1 -1
  20. package/dist/builtins/local-auth/local-auth.addon.mjs +1022 -33
  21. package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -1
  22. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +8 -0
  23. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts.map +1 -1
  24. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +27 -21
  25. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js.map +1 -1
  26. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +27 -21
  27. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +400 -41
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +400 -41
  33. package/dist/index.mjs.map +1 -1
  34. package/package.json +2 -1
@@ -229,14 +229,14 @@ var require_buffer_equal_constant_time = /* @__PURE__ */ require_chunk.__commonJ
229
229
  //#region ../../node_modules/jwa/index.js
230
230
  var require_jwa = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
231
231
  var Buffer = require_safe_buffer().Buffer;
232
- var crypto = require("crypto");
232
+ var crypto$1 = require("crypto");
233
233
  var formatEcdsa = require_ecdsa_sig_formatter();
234
234
  var util$2 = require("util");
235
235
  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\".";
236
236
  var MSG_INVALID_SECRET = "secret must be a string or buffer";
237
237
  var MSG_INVALID_VERIFIER_KEY = "key must be a string or a buffer";
238
238
  var MSG_INVALID_SIGNER_KEY = "key must be a string, a buffer or an object";
239
- var supportsKeyObjects = typeof crypto.createPublicKey === "function";
239
+ var supportsKeyObjects = typeof crypto$1.createPublicKey === "function";
240
240
  if (supportsKeyObjects) {
241
241
  MSG_INVALID_VERIFIER_KEY += " or a KeyObject";
242
242
  MSG_INVALID_SECRET += "or a KeyObject";
@@ -289,14 +289,14 @@ var require_jwa = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module)
289
289
  return function sign(thing, secret) {
290
290
  checkIsSecretKey(secret);
291
291
  thing = normalizeInput(thing);
292
- var hmac = crypto.createHmac("sha" + bits, secret);
292
+ var hmac = crypto$1.createHmac("sha" + bits, secret);
293
293
  return fromBase64((hmac.update(thing), hmac.digest("base64")));
294
294
  };
295
295
  }
296
296
  var bufferEqual;
297
- var timingSafeEqual = "timingSafeEqual" in crypto ? function timingSafeEqual(a, b) {
297
+ var timingSafeEqual = "timingSafeEqual" in crypto$1 ? function timingSafeEqual(a, b) {
298
298
  if (a.byteLength !== b.byteLength) return false;
299
- return crypto.timingSafeEqual(a, b);
299
+ return crypto$1.timingSafeEqual(a, b);
300
300
  } : function timingSafeEqual(a, b) {
301
301
  if (!bufferEqual) bufferEqual = require_buffer_equal_constant_time();
302
302
  return bufferEqual(a, b);
@@ -311,7 +311,7 @@ var require_jwa = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module)
311
311
  return function sign(thing, privateKey) {
312
312
  checkIsPrivateKey(privateKey);
313
313
  thing = normalizeInput(thing);
314
- var signer = crypto.createSign("RSA-SHA" + bits);
314
+ var signer = crypto$1.createSign("RSA-SHA" + bits);
315
315
  return fromBase64((signer.update(thing), signer.sign(privateKey, "base64")));
316
316
  };
317
317
  }
@@ -320,7 +320,7 @@ var require_jwa = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module)
320
320
  checkIsPublicKey(publicKey);
321
321
  thing = normalizeInput(thing);
322
322
  signature = toBase64(signature);
323
- var verifier = crypto.createVerify("RSA-SHA" + bits);
323
+ var verifier = crypto$1.createVerify("RSA-SHA" + bits);
324
324
  verifier.update(thing);
325
325
  return verifier.verify(publicKey, signature, "base64");
326
326
  };
@@ -329,11 +329,11 @@ var require_jwa = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module)
329
329
  return function sign(thing, privateKey) {
330
330
  checkIsPrivateKey(privateKey);
331
331
  thing = normalizeInput(thing);
332
- var signer = crypto.createSign("RSA-SHA" + bits);
332
+ var signer = crypto$1.createSign("RSA-SHA" + bits);
333
333
  return fromBase64((signer.update(thing), signer.sign({
334
334
  key: privateKey,
335
- padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
336
- saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST
335
+ padding: crypto$1.constants.RSA_PKCS1_PSS_PADDING,
336
+ saltLength: crypto$1.constants.RSA_PSS_SALTLEN_DIGEST
337
337
  }, "base64")));
338
338
  };
339
339
  }
@@ -342,12 +342,12 @@ var require_jwa = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module)
342
342
  checkIsPublicKey(publicKey);
343
343
  thing = normalizeInput(thing);
344
344
  signature = toBase64(signature);
345
- var verifier = crypto.createVerify("RSA-SHA" + bits);
345
+ var verifier = crypto$1.createVerify("RSA-SHA" + bits);
346
346
  verifier.update(thing);
347
347
  return verifier.verify({
348
348
  key: publicKey,
349
- padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
350
- saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST
349
+ padding: crypto$1.constants.RSA_PKCS1_PSS_PADDING,
350
+ saltLength: crypto$1.constants.RSA_PSS_SALTLEN_DIGEST
351
351
  }, signature, "base64");
352
352
  };
353
353
  }
@@ -6210,7 +6210,7 @@ var AuthManager = class {
6210
6210
  const payload = {
6211
6211
  userId: opts.agentId,
6212
6212
  username: opts.agentId,
6213
- role: opts.role ?? "agent",
6213
+ isAdmin: true,
6214
6214
  type: "service",
6215
6215
  agentId: opts.agentId,
6216
6216
  allowedProviders: "*",
@@ -6219,6 +6219,109 @@ var AuthManager = class {
6219
6219
  const expiresIn = opts.expiresIn ?? "24h";
6220
6220
  return import_jsonwebtoken.sign(payload, this.jwtSecret, { expiresIn });
6221
6221
  }
6222
+ /**
6223
+ * Mint a short-lived HMAC-signed bridge token used by SSO-style auth
6224
+ * providers (OIDC, SAML, magic-link, …) to hand the post-callback
6225
+ * claims to `/api/auth/sso/finish` without trusting unsigned query
6226
+ * parameters. The endpoint verifies the token signature with the same
6227
+ * `jwtSecret` and only then mints the user session JWT. This closes
6228
+ * the privilege-escalation gap where any client could craft a
6229
+ * `?isAdmin=1` link to `/sso/finish` and become admin.
6230
+ *
6231
+ * The payload carries `kind: 'sso-bridge'` so `verifySsoBridgeToken`
6232
+ * can reject session tokens reused as bridge tokens (and vice versa).
6233
+ * TTL defaults to 5 minutes — long enough to survive the IdP
6234
+ * redirect bounce, short enough that a leaked URL stops being useful
6235
+ * before the operator notices.
6236
+ */
6237
+ signSsoBridgeToken(payload, ttlSec = 300) {
6238
+ const body = {
6239
+ kind: "sso-bridge",
6240
+ userId: payload.userId,
6241
+ username: payload.username,
6242
+ isAdmin: payload.isAdmin,
6243
+ provider: payload.provider,
6244
+ ...payload.email !== void 0 ? { email: payload.email } : {},
6245
+ ...payload.displayName !== void 0 ? { displayName: payload.displayName } : {}
6246
+ };
6247
+ return import_jsonwebtoken.sign(body, this.jwtSecret, { expiresIn: ttlSec });
6248
+ }
6249
+ /**
6250
+ * Verify + decode a bridge token minted by `signSsoBridgeToken`.
6251
+ * Returns `null` for invalid signature, expired token, or wrong
6252
+ * `kind` claim. Never throws — the consumer ( `/sso/finish`) treats
6253
+ * `null` as 400 Bad Request.
6254
+ */
6255
+ verifySsoBridgeToken(token) {
6256
+ try {
6257
+ const decoded = import_jsonwebtoken.verify(token, this.jwtSecret);
6258
+ if (decoded["kind"] !== "sso-bridge") return null;
6259
+ const userId = decoded["userId"];
6260
+ const username = decoded["username"];
6261
+ const isAdmin = decoded["isAdmin"];
6262
+ const provider = decoded["provider"];
6263
+ if (typeof userId !== "string" || typeof username !== "string" || typeof isAdmin !== "boolean" || typeof provider !== "string") return null;
6264
+ const email = typeof decoded["email"] === "string" ? decoded["email"] : void 0;
6265
+ const displayName = typeof decoded["displayName"] === "string" ? decoded["displayName"] : void 0;
6266
+ return {
6267
+ userId,
6268
+ username,
6269
+ isAdmin,
6270
+ provider,
6271
+ ...email !== void 0 ? { email } : {},
6272
+ ...displayName !== void 0 ? { displayName } : {}
6273
+ };
6274
+ } catch {
6275
+ return null;
6276
+ }
6277
+ }
6278
+ /**
6279
+ * Mint a TOTP challenge token bridging the two-step login flow:
6280
+ *
6281
+ * 1. POST /login → password OK + user has 2FA → returns
6282
+ * `{requiresTotp: true, challengeToken: '...'}` (this token).
6283
+ * 2. POST /login/totp-verify → operator types 6-digit code →
6284
+ * server verifies challenge token + code → mints the real
6285
+ * session JWT.
6286
+ *
6287
+ * The token carries `kind: 'totp-challenge'` so it can't be confused
6288
+ * with a regular session token, plus the `userId` + `username` +
6289
+ * `isAdmin` claims that will end up in the final session if the code
6290
+ * verifies. TTL defaults to 5 minutes — long enough to type the code,
6291
+ * short enough that an abandoned login can't be picked up later.
6292
+ */
6293
+ signTotpChallengeToken(payload, ttlSec = 300) {
6294
+ return import_jsonwebtoken.sign({
6295
+ kind: "totp-challenge",
6296
+ userId: payload.userId,
6297
+ username: payload.username,
6298
+ isAdmin: payload.isAdmin
6299
+ }, this.jwtSecret, { expiresIn: ttlSec });
6300
+ }
6301
+ /**
6302
+ * Verify + decode a TOTP challenge token. Returns `null` for any
6303
+ * failure (invalid signature, expired, wrong `kind`, missing
6304
+ * claims). The caller MUST also verify the 6-digit code against
6305
+ * `totpManager.verify(userId, code)` before minting the real
6306
+ * session — this method validates the bridge transport only.
6307
+ */
6308
+ verifyTotpChallengeToken(token) {
6309
+ try {
6310
+ const decoded = import_jsonwebtoken.verify(token, this.jwtSecret);
6311
+ if (decoded["kind"] !== "totp-challenge") return null;
6312
+ const userId = decoded["userId"];
6313
+ const username = decoded["username"];
6314
+ const isAdmin = decoded["isAdmin"];
6315
+ if (typeof userId !== "string" || typeof username !== "string" || typeof isAdmin !== "boolean") return null;
6316
+ return {
6317
+ userId,
6318
+ username,
6319
+ isAdmin
6320
+ };
6321
+ } catch {
6322
+ return null;
6323
+ }
6324
+ }
6222
6325
  };
6223
6326
  //#endregion
6224
6327
  //#region src/auth/parse-record.ts
@@ -6255,7 +6358,7 @@ var UserManager = class {
6255
6358
  id: node_crypto.randomUUID(),
6256
6359
  username: input.username,
6257
6360
  passwordHash,
6258
- role: input.role,
6361
+ isAdmin: input.isAdmin ?? false,
6259
6362
  allowedProviders: input.allowedProviders ?? "*",
6260
6363
  allowedDevices: input.allowedDevices ?? {},
6261
6364
  scopes: input.scopes ?? [],
@@ -6343,7 +6446,7 @@ var UserManager = class {
6343
6446
  await this.create({
6344
6447
  username: adminUsername,
6345
6448
  password: adminPassword,
6346
- role: "admin",
6449
+ isAdmin: true,
6347
6450
  allowedProviders: "*",
6348
6451
  allowedDevices: {}
6349
6452
  });
@@ -6369,7 +6472,7 @@ var ApiKeyManager = class {
6369
6472
  const record = {
6370
6473
  id: node_crypto.randomUUID(),
6371
6474
  label: input.label,
6372
- role: input.role,
6475
+ isAdmin: input.isAdmin ?? false,
6373
6476
  allowedProviders: input.allowedProviders ?? "*",
6374
6477
  allowedDevices: input.allowedDevices ?? {},
6375
6478
  tokenHash: hash,
@@ -6563,10 +6666,841 @@ var ScopedTokenManager = class {
6563
6666
  }
6564
6667
  };
6565
6668
  //#endregion
6669
+ //#region ../../node_modules/@otplib/plugin-crypto/index.js
6670
+ /**
6671
+ * @otplib/plugin-crypto
6672
+ *
6673
+ * @author Gerald Yeo <contact@fusedthought.com>
6674
+ * @version: 12.0.1
6675
+ * @license: MIT
6676
+ **/
6677
+ var require_plugin_crypto = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
6678
+ Object.defineProperty(exports, "__esModule", { value: true });
6679
+ function _interopDefault(ex) {
6680
+ return ex && typeof ex === "object" && "default" in ex ? ex["default"] : ex;
6681
+ }
6682
+ var crypto = _interopDefault(require("crypto"));
6683
+ var createDigest = (algorithm, hmacKey, counter) => {
6684
+ return crypto.createHmac(algorithm, Buffer.from(hmacKey, "hex")).update(Buffer.from(counter, "hex")).digest().toString("hex");
6685
+ };
6686
+ var createRandomBytes = (size, encoding) => {
6687
+ return crypto.randomBytes(size).toString(encoding);
6688
+ };
6689
+ exports.createDigest = createDigest;
6690
+ exports.createRandomBytes = createRandomBytes;
6691
+ }));
6692
+ //#endregion
6693
+ //#region ../../node_modules/thirty-two/lib/thirty-two/thirty-two.js
6694
+ var require_thirty_two$1 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
6695
+ var charTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
6696
+ var byteTable = [
6697
+ 255,
6698
+ 255,
6699
+ 26,
6700
+ 27,
6701
+ 28,
6702
+ 29,
6703
+ 30,
6704
+ 31,
6705
+ 255,
6706
+ 255,
6707
+ 255,
6708
+ 255,
6709
+ 255,
6710
+ 255,
6711
+ 255,
6712
+ 255,
6713
+ 255,
6714
+ 0,
6715
+ 1,
6716
+ 2,
6717
+ 3,
6718
+ 4,
6719
+ 5,
6720
+ 6,
6721
+ 7,
6722
+ 8,
6723
+ 9,
6724
+ 10,
6725
+ 11,
6726
+ 12,
6727
+ 13,
6728
+ 14,
6729
+ 15,
6730
+ 16,
6731
+ 17,
6732
+ 18,
6733
+ 19,
6734
+ 20,
6735
+ 21,
6736
+ 22,
6737
+ 23,
6738
+ 24,
6739
+ 25,
6740
+ 255,
6741
+ 255,
6742
+ 255,
6743
+ 255,
6744
+ 255,
6745
+ 255,
6746
+ 0,
6747
+ 1,
6748
+ 2,
6749
+ 3,
6750
+ 4,
6751
+ 5,
6752
+ 6,
6753
+ 7,
6754
+ 8,
6755
+ 9,
6756
+ 10,
6757
+ 11,
6758
+ 12,
6759
+ 13,
6760
+ 14,
6761
+ 15,
6762
+ 16,
6763
+ 17,
6764
+ 18,
6765
+ 19,
6766
+ 20,
6767
+ 21,
6768
+ 22,
6769
+ 23,
6770
+ 24,
6771
+ 25,
6772
+ 255,
6773
+ 255,
6774
+ 255,
6775
+ 255,
6776
+ 255
6777
+ ];
6778
+ function quintetCount(buff) {
6779
+ var quintets = Math.floor(buff.length / 5);
6780
+ return buff.length % 5 === 0 ? quintets : quintets + 1;
6781
+ }
6782
+ exports.encode = function(plain) {
6783
+ if (!Buffer.isBuffer(plain)) plain = new Buffer(plain);
6784
+ var i = 0;
6785
+ var j = 0;
6786
+ var shiftIndex = 0;
6787
+ var digit = 0;
6788
+ var encoded = new Buffer(quintetCount(plain) * 8);
6789
+ while (i < plain.length) {
6790
+ var current = plain[i];
6791
+ if (shiftIndex > 3) {
6792
+ digit = current & 255 >> shiftIndex;
6793
+ shiftIndex = (shiftIndex + 5) % 8;
6794
+ digit = digit << shiftIndex | (i + 1 < plain.length ? plain[i + 1] : 0) >> 8 - shiftIndex;
6795
+ i++;
6796
+ } else {
6797
+ digit = current >> 8 - (shiftIndex + 5) & 31;
6798
+ shiftIndex = (shiftIndex + 5) % 8;
6799
+ if (shiftIndex === 0) i++;
6800
+ }
6801
+ encoded[j] = charTable.charCodeAt(digit);
6802
+ j++;
6803
+ }
6804
+ for (i = j; i < encoded.length; i++) encoded[i] = 61;
6805
+ return encoded;
6806
+ };
6807
+ exports.decode = function(encoded) {
6808
+ var shiftIndex = 0;
6809
+ var plainDigit = 0;
6810
+ var plainChar;
6811
+ var plainPos = 0;
6812
+ if (!Buffer.isBuffer(encoded)) encoded = new Buffer(encoded);
6813
+ var decoded = new Buffer(Math.ceil(encoded.length * 5 / 8));
6814
+ for (var i = 0; i < encoded.length; i++) {
6815
+ if (encoded[i] === 61) break;
6816
+ var encodedByte = encoded[i] - 48;
6817
+ if (encodedByte < byteTable.length) {
6818
+ plainDigit = byteTable[encodedByte];
6819
+ if (shiftIndex <= 3) {
6820
+ shiftIndex = (shiftIndex + 5) % 8;
6821
+ if (shiftIndex === 0) {
6822
+ plainChar |= plainDigit;
6823
+ decoded[plainPos] = plainChar;
6824
+ plainPos++;
6825
+ plainChar = 0;
6826
+ } else plainChar |= 255 & plainDigit << 8 - shiftIndex;
6827
+ } else {
6828
+ shiftIndex = (shiftIndex + 5) % 8;
6829
+ plainChar |= 255 & plainDigit >>> shiftIndex;
6830
+ decoded[plainPos] = plainChar;
6831
+ plainPos++;
6832
+ plainChar = 255 & plainDigit << 8 - shiftIndex;
6833
+ }
6834
+ } else throw new Error("Invalid input - it is not base32 encoded string");
6835
+ }
6836
+ return decoded.slice(0, plainPos);
6837
+ };
6838
+ }));
6839
+ //#endregion
6840
+ //#region ../../node_modules/thirty-two/lib/thirty-two/index.js
6841
+ var require_thirty_two = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
6842
+ var base32 = require_thirty_two$1();
6843
+ exports.encode = base32.encode;
6844
+ exports.decode = base32.decode;
6845
+ }));
6846
+ //#endregion
6847
+ //#region ../../node_modules/@otplib/plugin-thirty-two/index.js
6848
+ /**
6849
+ * @otplib/plugin-thirty-two
6850
+ *
6851
+ * @author Gerald Yeo <contact@fusedthought.com>
6852
+ * @version: 12.0.1
6853
+ * @license: MIT
6854
+ **/
6855
+ var require_plugin_thirty_two = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
6856
+ Object.defineProperty(exports, "__esModule", { value: true });
6857
+ function _interopDefault(ex) {
6858
+ return ex && typeof ex === "object" && "default" in ex ? ex["default"] : ex;
6859
+ }
6860
+ var thirtyTwo = _interopDefault(require_thirty_two());
6861
+ var keyDecoder = (encodedSecret, encoding) => {
6862
+ return thirtyTwo.decode(encodedSecret).toString(encoding);
6863
+ };
6864
+ var keyEncoder = (secret, encoding) => {
6865
+ return thirtyTwo.encode(Buffer.from(secret, encoding).toString("ascii")).toString().replace(/=/g, "");
6866
+ };
6867
+ exports.keyDecoder = keyDecoder;
6868
+ exports.keyEncoder = keyEncoder;
6869
+ }));
6870
+ //#endregion
6871
+ //#region ../../node_modules/@otplib/core/index.js
6872
+ /**
6873
+ * @otplib/core
6874
+ *
6875
+ * @author Gerald Yeo <contact@fusedthought.com>
6876
+ * @version: 12.0.1
6877
+ * @license: MIT
6878
+ **/
6879
+ var require_core = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
6880
+ Object.defineProperty(exports, "__esModule", { value: true });
6881
+ function objectValues(value) {
6882
+ return Object.keys(value).map((key) => value[key]);
6883
+ }
6884
+ (function(HashAlgorithms) {
6885
+ HashAlgorithms["SHA1"] = "sha1";
6886
+ HashAlgorithms["SHA256"] = "sha256";
6887
+ HashAlgorithms["SHA512"] = "sha512";
6888
+ })(exports.HashAlgorithms || (exports.HashAlgorithms = {}));
6889
+ var HASH_ALGORITHMS = objectValues(exports.HashAlgorithms);
6890
+ (function(KeyEncodings) {
6891
+ KeyEncodings["ASCII"] = "ascii";
6892
+ KeyEncodings["BASE64"] = "base64";
6893
+ KeyEncodings["HEX"] = "hex";
6894
+ KeyEncodings["LATIN1"] = "latin1";
6895
+ KeyEncodings["UTF8"] = "utf8";
6896
+ })(exports.KeyEncodings || (exports.KeyEncodings = {}));
6897
+ var KEY_ENCODINGS = objectValues(exports.KeyEncodings);
6898
+ (function(Strategy) {
6899
+ Strategy["HOTP"] = "hotp";
6900
+ Strategy["TOTP"] = "totp";
6901
+ })(exports.Strategy || (exports.Strategy = {}));
6902
+ var STRATEGY = objectValues(exports.Strategy);
6903
+ var createDigestPlaceholder = () => {
6904
+ throw new Error("Please provide an options.createDigest implementation.");
6905
+ };
6906
+ function isTokenValid(value) {
6907
+ return /^(\d+)$/.test(value);
6908
+ }
6909
+ function padStart(value, maxLength, fillString) {
6910
+ if (value.length >= maxLength) return value;
6911
+ return `${Array(maxLength + 1).join(fillString)}${value}`.slice(-1 * maxLength);
6912
+ }
6913
+ function keyuri(options) {
6914
+ const tmpl = `otpauth://${options.type}/{labelPrefix}:{accountName}?secret={secret}{query}`;
6915
+ const params = [];
6916
+ if (STRATEGY.indexOf(options.type) < 0) throw new Error(`Expecting options.type to be one of ${STRATEGY.join(", ")}. Received ${options.type}.`);
6917
+ if (options.type === "hotp") {
6918
+ if (options.counter == null || typeof options.counter !== "number") throw new Error("Expecting options.counter to be a number when options.type is \"hotp\".");
6919
+ params.push(`&counter=${options.counter}`);
6920
+ }
6921
+ if (options.type === "totp" && options.step) params.push(`&period=${options.step}`);
6922
+ if (options.digits) params.push(`&digits=${options.digits}`);
6923
+ if (options.algorithm) params.push(`&algorithm=${options.algorithm.toUpperCase()}`);
6924
+ if (options.issuer) params.push(`&issuer=${encodeURIComponent(options.issuer)}`);
6925
+ return tmpl.replace("{labelPrefix}", encodeURIComponent(options.issuer || options.accountName)).replace("{accountName}", encodeURIComponent(options.accountName)).replace("{secret}", options.secret).replace("{query}", params.join(""));
6926
+ }
6927
+ var OTP = class OTP {
6928
+ constructor(defaultOptions = {}) {
6929
+ this._defaultOptions = Object.freeze({ ...defaultOptions });
6930
+ this._options = Object.freeze({});
6931
+ }
6932
+ create(defaultOptions = {}) {
6933
+ return new OTP(defaultOptions);
6934
+ }
6935
+ clone(defaultOptions = {}) {
6936
+ const instance = this.create({
6937
+ ...this._defaultOptions,
6938
+ ...defaultOptions
6939
+ });
6940
+ instance.options = this._options;
6941
+ return instance;
6942
+ }
6943
+ get options() {
6944
+ return Object.freeze({
6945
+ ...this._defaultOptions,
6946
+ ...this._options
6947
+ });
6948
+ }
6949
+ set options(options) {
6950
+ this._options = Object.freeze({
6951
+ ...this._options,
6952
+ ...options
6953
+ });
6954
+ }
6955
+ allOptions() {
6956
+ return this.options;
6957
+ }
6958
+ resetOptions() {
6959
+ this._options = Object.freeze({});
6960
+ }
6961
+ };
6962
+ function hotpOptionsValidator(options) {
6963
+ if (typeof options.createDigest !== "function") throw new Error("Expecting options.createDigest to be a function.");
6964
+ if (typeof options.createHmacKey !== "function") throw new Error("Expecting options.createHmacKey to be a function.");
6965
+ if (typeof options.digits !== "number") throw new Error("Expecting options.digits to be a number.");
6966
+ if (!options.algorithm || HASH_ALGORITHMS.indexOf(options.algorithm) < 0) throw new Error(`Expecting options.algorithm to be one of ${HASH_ALGORITHMS.join(", ")}. Received ${options.algorithm}.`);
6967
+ if (!options.encoding || KEY_ENCODINGS.indexOf(options.encoding) < 0) throw new Error(`Expecting options.encoding to be one of ${KEY_ENCODINGS.join(", ")}. Received ${options.encoding}.`);
6968
+ }
6969
+ var hotpCreateHmacKey = (algorithm, secret, encoding) => {
6970
+ return Buffer.from(secret, encoding).toString("hex");
6971
+ };
6972
+ function hotpDefaultOptions() {
6973
+ return {
6974
+ algorithm: exports.HashAlgorithms.SHA1,
6975
+ createHmacKey: hotpCreateHmacKey,
6976
+ createDigest: createDigestPlaceholder,
6977
+ digits: 6,
6978
+ encoding: exports.KeyEncodings.ASCII
6979
+ };
6980
+ }
6981
+ function hotpOptions(opt) {
6982
+ const options = {
6983
+ ...hotpDefaultOptions(),
6984
+ ...opt
6985
+ };
6986
+ hotpOptionsValidator(options);
6987
+ return Object.freeze(options);
6988
+ }
6989
+ function hotpCounter(counter) {
6990
+ return padStart(counter.toString(16), 16, "0");
6991
+ }
6992
+ function hotpDigestToToken(hexDigest, digits) {
6993
+ const digest = Buffer.from(hexDigest, "hex");
6994
+ const offset = digest[digest.length - 1] & 15;
6995
+ const token = ((digest[offset] & 127) << 24 | (digest[offset + 1] & 255) << 16 | (digest[offset + 2] & 255) << 8 | digest[offset + 3] & 255) % Math.pow(10, digits);
6996
+ return padStart(String(token), digits, "0");
6997
+ }
6998
+ function hotpDigest(secret, counter, options) {
6999
+ const hexCounter = hotpCounter(counter);
7000
+ const hmacKey = options.createHmacKey(options.algorithm, secret, options.encoding);
7001
+ return options.createDigest(options.algorithm, hmacKey, hexCounter);
7002
+ }
7003
+ function hotpToken(secret, counter, options) {
7004
+ return hotpDigestToToken(options.digest || hotpDigest(secret, counter, options), options.digits);
7005
+ }
7006
+ function hotpCheck(token, secret, counter, options) {
7007
+ if (!isTokenValid(token)) return false;
7008
+ return token === hotpToken(secret, counter, options);
7009
+ }
7010
+ function hotpKeyuri(accountName, issuer, secret, counter, options) {
7011
+ return keyuri({
7012
+ algorithm: options.algorithm,
7013
+ digits: options.digits,
7014
+ type: exports.Strategy.HOTP,
7015
+ accountName,
7016
+ counter,
7017
+ issuer,
7018
+ secret
7019
+ });
7020
+ }
7021
+ var HOTP = class HOTP extends OTP {
7022
+ create(defaultOptions = {}) {
7023
+ return new HOTP(defaultOptions);
7024
+ }
7025
+ allOptions() {
7026
+ return hotpOptions(this.options);
7027
+ }
7028
+ generate(secret, counter) {
7029
+ return hotpToken(secret, counter, this.allOptions());
7030
+ }
7031
+ check(token, secret, counter) {
7032
+ return hotpCheck(token, secret, counter, this.allOptions());
7033
+ }
7034
+ verify(opts) {
7035
+ if (typeof opts !== "object") throw new Error("Expecting argument 0 of verify to be an object");
7036
+ return this.check(opts.token, opts.secret, opts.counter);
7037
+ }
7038
+ keyuri(accountName, issuer, secret, counter) {
7039
+ return hotpKeyuri(accountName, issuer, secret, counter, this.allOptions());
7040
+ }
7041
+ };
7042
+ function parseWindowBounds(win) {
7043
+ if (typeof win === "number") return [Math.abs(win), Math.abs(win)];
7044
+ if (Array.isArray(win)) {
7045
+ const [past, future] = win;
7046
+ if (typeof past === "number" && typeof future === "number") return [Math.abs(past), Math.abs(future)];
7047
+ }
7048
+ throw new Error("Expecting options.window to be an number or [number, number].");
7049
+ }
7050
+ function totpOptionsValidator(options) {
7051
+ hotpOptionsValidator(options);
7052
+ parseWindowBounds(options.window);
7053
+ if (typeof options.epoch !== "number") throw new Error("Expecting options.epoch to be a number.");
7054
+ if (typeof options.step !== "number") throw new Error("Expecting options.step to be a number.");
7055
+ }
7056
+ var totpPadSecret = (secret, encoding, minLength) => {
7057
+ const currentLength = secret.length;
7058
+ const hexSecret = Buffer.from(secret, encoding).toString("hex");
7059
+ if (currentLength < minLength) {
7060
+ const newSecret = new Array(minLength - currentLength + 1).join(hexSecret);
7061
+ return Buffer.from(newSecret, "hex").slice(0, minLength).toString("hex");
7062
+ }
7063
+ return hexSecret;
7064
+ };
7065
+ var totpCreateHmacKey = (algorithm, secret, encoding) => {
7066
+ switch (algorithm) {
7067
+ case exports.HashAlgorithms.SHA1: return totpPadSecret(secret, encoding, 20);
7068
+ case exports.HashAlgorithms.SHA256: return totpPadSecret(secret, encoding, 32);
7069
+ case exports.HashAlgorithms.SHA512: return totpPadSecret(secret, encoding, 64);
7070
+ default: throw new Error(`Expecting algorithm to be one of ${HASH_ALGORITHMS.join(", ")}. Received ${algorithm}.`);
7071
+ }
7072
+ };
7073
+ function totpDefaultOptions() {
7074
+ return {
7075
+ algorithm: exports.HashAlgorithms.SHA1,
7076
+ createDigest: createDigestPlaceholder,
7077
+ createHmacKey: totpCreateHmacKey,
7078
+ digits: 6,
7079
+ encoding: exports.KeyEncodings.ASCII,
7080
+ epoch: Date.now(),
7081
+ step: 30,
7082
+ window: 0
7083
+ };
7084
+ }
7085
+ function totpOptions(opt) {
7086
+ const options = {
7087
+ ...totpDefaultOptions(),
7088
+ ...opt
7089
+ };
7090
+ totpOptionsValidator(options);
7091
+ return Object.freeze(options);
7092
+ }
7093
+ function totpCounter(epoch, step) {
7094
+ return Math.floor(epoch / step / 1e3);
7095
+ }
7096
+ function totpToken(secret, options) {
7097
+ return hotpToken(secret, totpCounter(options.epoch, options.step), options);
7098
+ }
7099
+ function totpEpochsInWindow(epoch, direction, deltaPerEpoch, numOfEpoches) {
7100
+ const result = [];
7101
+ if (numOfEpoches === 0) return result;
7102
+ for (let i = 1; i <= numOfEpoches; i++) {
7103
+ const delta = direction * i * deltaPerEpoch;
7104
+ result.push(epoch + delta);
7105
+ }
7106
+ return result;
7107
+ }
7108
+ function totpEpochAvailable(epoch, step, win) {
7109
+ const bounds = parseWindowBounds(win);
7110
+ const delta = step * 1e3;
7111
+ return {
7112
+ current: epoch,
7113
+ past: totpEpochsInWindow(epoch, -1, delta, bounds[0]),
7114
+ future: totpEpochsInWindow(epoch, 1, delta, bounds[1])
7115
+ };
7116
+ }
7117
+ function totpCheck(token, secret, options) {
7118
+ if (!isTokenValid(token)) return false;
7119
+ return token === totpToken(secret, options);
7120
+ }
7121
+ function totpCheckByEpoch(epochs, token, secret, options) {
7122
+ let position = null;
7123
+ epochs.some((epoch, idx) => {
7124
+ if (totpCheck(token, secret, {
7125
+ ...options,
7126
+ epoch
7127
+ })) {
7128
+ position = idx + 1;
7129
+ return true;
7130
+ }
7131
+ return false;
7132
+ });
7133
+ return position;
7134
+ }
7135
+ function totpCheckWithWindow(token, secret, options) {
7136
+ if (totpCheck(token, secret, options)) return 0;
7137
+ const epochs = totpEpochAvailable(options.epoch, options.step, options.window);
7138
+ const backward = totpCheckByEpoch(epochs.past, token, secret, options);
7139
+ if (backward !== null) return backward * -1;
7140
+ return totpCheckByEpoch(epochs.future, token, secret, options);
7141
+ }
7142
+ function totpTimeUsed(epoch, step) {
7143
+ return Math.floor(epoch / 1e3) % step;
7144
+ }
7145
+ function totpTimeRemaining(epoch, step) {
7146
+ return step - totpTimeUsed(epoch, step);
7147
+ }
7148
+ function totpKeyuri(accountName, issuer, secret, options) {
7149
+ return keyuri({
7150
+ algorithm: options.algorithm,
7151
+ digits: options.digits,
7152
+ step: options.step,
7153
+ type: exports.Strategy.TOTP,
7154
+ accountName,
7155
+ issuer,
7156
+ secret
7157
+ });
7158
+ }
7159
+ var TOTP = class TOTP extends HOTP {
7160
+ create(defaultOptions = {}) {
7161
+ return new TOTP(defaultOptions);
7162
+ }
7163
+ allOptions() {
7164
+ return totpOptions(this.options);
7165
+ }
7166
+ generate(secret) {
7167
+ return totpToken(secret, this.allOptions());
7168
+ }
7169
+ checkDelta(token, secret) {
7170
+ return totpCheckWithWindow(token, secret, this.allOptions());
7171
+ }
7172
+ check(token, secret) {
7173
+ return typeof this.checkDelta(token, secret) === "number";
7174
+ }
7175
+ verify(opts) {
7176
+ if (typeof opts !== "object") throw new Error("Expecting argument 0 of verify to be an object");
7177
+ return this.check(opts.token, opts.secret);
7178
+ }
7179
+ timeRemaining() {
7180
+ const options = this.allOptions();
7181
+ return totpTimeRemaining(options.epoch, options.step);
7182
+ }
7183
+ timeUsed() {
7184
+ const options = this.allOptions();
7185
+ return totpTimeUsed(options.epoch, options.step);
7186
+ }
7187
+ keyuri(accountName, issuer, secret) {
7188
+ return totpKeyuri(accountName, issuer, secret, this.allOptions());
7189
+ }
7190
+ };
7191
+ function authenticatorOptionValidator(options) {
7192
+ totpOptionsValidator(options);
7193
+ if (typeof options.keyDecoder !== "function") throw new Error("Expecting options.keyDecoder to be a function.");
7194
+ if (options.keyEncoder && typeof options.keyEncoder !== "function") throw new Error("Expecting options.keyEncoder to be a function.");
7195
+ }
7196
+ function authenticatorDefaultOptions() {
7197
+ return {
7198
+ algorithm: exports.HashAlgorithms.SHA1,
7199
+ createDigest: createDigestPlaceholder,
7200
+ createHmacKey: totpCreateHmacKey,
7201
+ digits: 6,
7202
+ encoding: exports.KeyEncodings.HEX,
7203
+ epoch: Date.now(),
7204
+ step: 30,
7205
+ window: 0
7206
+ };
7207
+ }
7208
+ function authenticatorOptions(opt) {
7209
+ const options = {
7210
+ ...authenticatorDefaultOptions(),
7211
+ ...opt
7212
+ };
7213
+ authenticatorOptionValidator(options);
7214
+ return Object.freeze(options);
7215
+ }
7216
+ function authenticatorEncoder(secret, options) {
7217
+ return options.keyEncoder(secret, options.encoding);
7218
+ }
7219
+ function authenticatorDecoder(secret, options) {
7220
+ return options.keyDecoder(secret, options.encoding);
7221
+ }
7222
+ function authenticatorGenerateSecret(numberOfBytes, options) {
7223
+ return authenticatorEncoder(options.createRandomBytes(numberOfBytes, options.encoding), options);
7224
+ }
7225
+ function authenticatorToken(secret, options) {
7226
+ return totpToken(authenticatorDecoder(secret, options), options);
7227
+ }
7228
+ function authenticatorCheckWithWindow(token, secret, options) {
7229
+ return totpCheckWithWindow(token, authenticatorDecoder(secret, options), options);
7230
+ }
7231
+ exports.Authenticator = class Authenticator extends TOTP {
7232
+ create(defaultOptions = {}) {
7233
+ return new Authenticator(defaultOptions);
7234
+ }
7235
+ allOptions() {
7236
+ return authenticatorOptions(this.options);
7237
+ }
7238
+ generate(secret) {
7239
+ return authenticatorToken(secret, this.allOptions());
7240
+ }
7241
+ checkDelta(token, secret) {
7242
+ return authenticatorCheckWithWindow(token, secret, this.allOptions());
7243
+ }
7244
+ encode(secret) {
7245
+ return authenticatorEncoder(secret, this.allOptions());
7246
+ }
7247
+ decode(secret) {
7248
+ return authenticatorDecoder(secret, this.allOptions());
7249
+ }
7250
+ generateSecret(numberOfBytes = 10) {
7251
+ return authenticatorGenerateSecret(numberOfBytes, this.allOptions());
7252
+ }
7253
+ };
7254
+ exports.HASH_ALGORITHMS = HASH_ALGORITHMS;
7255
+ exports.HOTP = HOTP;
7256
+ exports.KEY_ENCODINGS = KEY_ENCODINGS;
7257
+ exports.OTP = OTP;
7258
+ exports.STRATEGY = STRATEGY;
7259
+ exports.TOTP = TOTP;
7260
+ exports.authenticatorCheckWithWindow = authenticatorCheckWithWindow;
7261
+ exports.authenticatorDecoder = authenticatorDecoder;
7262
+ exports.authenticatorDefaultOptions = authenticatorDefaultOptions;
7263
+ exports.authenticatorEncoder = authenticatorEncoder;
7264
+ exports.authenticatorGenerateSecret = authenticatorGenerateSecret;
7265
+ exports.authenticatorOptionValidator = authenticatorOptionValidator;
7266
+ exports.authenticatorOptions = authenticatorOptions;
7267
+ exports.authenticatorToken = authenticatorToken;
7268
+ exports.createDigestPlaceholder = createDigestPlaceholder;
7269
+ exports.hotpCheck = hotpCheck;
7270
+ exports.hotpCounter = hotpCounter;
7271
+ exports.hotpCreateHmacKey = hotpCreateHmacKey;
7272
+ exports.hotpDefaultOptions = hotpDefaultOptions;
7273
+ exports.hotpDigestToToken = hotpDigestToToken;
7274
+ exports.hotpKeyuri = hotpKeyuri;
7275
+ exports.hotpOptions = hotpOptions;
7276
+ exports.hotpOptionsValidator = hotpOptionsValidator;
7277
+ exports.hotpToken = hotpToken;
7278
+ exports.isTokenValid = isTokenValid;
7279
+ exports.keyuri = keyuri;
7280
+ exports.objectValues = objectValues;
7281
+ exports.padStart = padStart;
7282
+ exports.totpCheck = totpCheck;
7283
+ exports.totpCheckByEpoch = totpCheckByEpoch;
7284
+ exports.totpCheckWithWindow = totpCheckWithWindow;
7285
+ exports.totpCounter = totpCounter;
7286
+ exports.totpCreateHmacKey = totpCreateHmacKey;
7287
+ exports.totpDefaultOptions = totpDefaultOptions;
7288
+ exports.totpEpochAvailable = totpEpochAvailable;
7289
+ exports.totpKeyuri = totpKeyuri;
7290
+ exports.totpOptions = totpOptions;
7291
+ exports.totpOptionsValidator = totpOptionsValidator;
7292
+ exports.totpPadSecret = totpPadSecret;
7293
+ exports.totpTimeRemaining = totpTimeRemaining;
7294
+ exports.totpTimeUsed = totpTimeUsed;
7295
+ exports.totpToken = totpToken;
7296
+ }));
7297
+ //#endregion
7298
+ //#region ../../node_modules/@otplib/preset-default/index.js
7299
+ /**
7300
+ * @otplib/preset-default
7301
+ *
7302
+ * @author Gerald Yeo <contact@fusedthought.com>
7303
+ * @version: 12.0.1
7304
+ * @license: MIT
7305
+ **/
7306
+ var require_preset_default = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
7307
+ Object.defineProperty(exports, "__esModule", { value: true });
7308
+ var pluginCrypto = require_plugin_crypto();
7309
+ var pluginThirtyTwo = require_plugin_thirty_two();
7310
+ var core = require_core();
7311
+ var hotp = new core.HOTP({ createDigest: pluginCrypto.createDigest });
7312
+ var totp = new core.TOTP({ createDigest: pluginCrypto.createDigest });
7313
+ exports.authenticator = new core.Authenticator({
7314
+ createDigest: pluginCrypto.createDigest,
7315
+ createRandomBytes: pluginCrypto.createRandomBytes,
7316
+ keyDecoder: pluginThirtyTwo.keyDecoder,
7317
+ keyEncoder: pluginThirtyTwo.keyEncoder
7318
+ });
7319
+ exports.hotp = hotp;
7320
+ exports.totp = totp;
7321
+ }));
7322
+ //#endregion
7323
+ //#region src/auth/totp-manager.ts
7324
+ var import_otplib = (/* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
7325
+ Object.defineProperty(exports, "__esModule", { value: true });
7326
+ var presetDefault = require_preset_default();
7327
+ Object.keys(presetDefault).forEach(function(k) {
7328
+ if (k !== "default") Object.defineProperty(exports, k, {
7329
+ enumerable: true,
7330
+ get: function() {
7331
+ return presetDefault[k];
7332
+ }
7333
+ });
7334
+ });
7335
+ })))();
7336
+ /**
7337
+ * TOTP / 2FA enrollment manager.
7338
+ *
7339
+ * Three-state lifecycle keyed by `userId` (PK of `user_totp` collection):
7340
+ *
7341
+ * - No row → not set up. `setup()` inserts a row with a
7342
+ * fresh secret and `confirmedAt = null`.
7343
+ * - `confirmedAt = null` → setup in progress; user has the secret but
7344
+ * hasn't proved possession of the
7345
+ * authenticator yet. `verify()` returns
7346
+ * false even with a valid code — the row is
7347
+ * inert until confirmed.
7348
+ * - `confirmedAt != null` → enrolled; `verify()` accepts codes.
7349
+ *
7350
+ * Disable removes the row entirely. The next `setup()` starts fresh —
7351
+ * we do NOT reuse the old secret on re-enroll (avoids the case where a
7352
+ * leaked secret stays valid after a "rotation").
7353
+ *
7354
+ * The shared `authenticator` from `otplib` uses RFC 6238 TOTP with the
7355
+ * default 30-second window. We bump the validation window from 1 step
7356
+ * to 1 step in either direction (±30s) — operator clock skew is the
7357
+ * usual culprit for "valid code rejected" complaints.
7358
+ */
7359
+ var TOTP_COLLECTION = "user_totp";
7360
+ var ISSUER = "CamStack";
7361
+ function parseRow(data) {
7362
+ const userId = data["userId"];
7363
+ const secret = data["secret"];
7364
+ const confirmedAt = data["confirmedAt"];
7365
+ const createdAt = data["createdAt"];
7366
+ if (typeof userId !== "string") throw new Error("user_totp row: missing userId");
7367
+ if (typeof secret !== "string") throw new Error("user_totp row: missing secret");
7368
+ if (typeof createdAt !== "number") throw new Error("user_totp row: missing createdAt");
7369
+ return {
7370
+ userId,
7371
+ secret,
7372
+ confirmedAt: typeof confirmedAt === "number" ? confirmedAt : null,
7373
+ createdAt
7374
+ };
7375
+ }
7376
+ var TotpManager = class {
7377
+ constructor(store, opts = {}) {
7378
+ this.store = store;
7379
+ this.opts = opts;
7380
+ import_otplib.authenticator.options = { window: opts.window ?? 1 };
7381
+ }
7382
+ /**
7383
+ * Begin enrollment. Always generates a fresh secret — calling `setup`
7384
+ * a second time on the same user replaces any pending half-enrollment
7385
+ * (the user must rescan the QR / re-enter the secret).
7386
+ *
7387
+ * The user identity (`label`) is what an authenticator displays as
7388
+ * the account name; we use the supplied `username` for human-readable
7389
+ * recognition. `ISSUER` is the CamStack brand.
7390
+ */
7391
+ async setup(userId, username) {
7392
+ const secret = import_otplib.authenticator.generateSecret();
7393
+ const otpauthUrl = import_otplib.authenticator.keyuri(username, ISSUER, secret);
7394
+ const row = {
7395
+ userId,
7396
+ secret,
7397
+ confirmedAt: null,
7398
+ createdAt: Date.now()
7399
+ };
7400
+ await this.store.delete.mutate({
7401
+ collection: TOTP_COLLECTION,
7402
+ key: userId
7403
+ });
7404
+ await this.store.insert.mutate({
7405
+ collection: TOTP_COLLECTION,
7406
+ record: {
7407
+ id: userId,
7408
+ data: { ...row }
7409
+ }
7410
+ });
7411
+ return {
7412
+ secret,
7413
+ otpauthUrl
7414
+ };
7415
+ }
7416
+ /**
7417
+ * Confirm enrollment by checking a code from the authenticator
7418
+ * against the pending secret. On success, flips `confirmedAt` so
7419
+ * `verify()` starts accepting codes. Idempotent on already-confirmed
7420
+ * rows: re-confirming a valid code just succeeds with no state
7421
+ * change.
7422
+ */
7423
+ async confirm(userId, code) {
7424
+ const row = await this.find(userId);
7425
+ if (!row) return false;
7426
+ if (!import_otplib.authenticator.check(code, row.secret)) return false;
7427
+ if (row.confirmedAt === null) {
7428
+ const next = {
7429
+ ...row,
7430
+ confirmedAt: Date.now()
7431
+ };
7432
+ await this.store.update.mutate({
7433
+ collection: TOTP_COLLECTION,
7434
+ id: userId,
7435
+ data: { ...next }
7436
+ });
7437
+ }
7438
+ return true;
7439
+ }
7440
+ /**
7441
+ * Remove enrollment. Idempotent — no error if the user had no row.
7442
+ */
7443
+ async disable(userId) {
7444
+ await this.store.delete.mutate({
7445
+ collection: TOTP_COLLECTION,
7446
+ key: userId
7447
+ });
7448
+ }
7449
+ /**
7450
+ * Verify a code against the confirmed secret. Returns `false` when:
7451
+ * - The user has no row (not set up).
7452
+ * - The row is pending (`confirmedAt === null`).
7453
+ * - The code doesn't match within the configured window.
7454
+ */
7455
+ async verify(userId, code) {
7456
+ const row = await this.find(userId);
7457
+ if (!row) return false;
7458
+ if (row.confirmedAt === null) return false;
7459
+ return import_otplib.authenticator.check(code, row.secret);
7460
+ }
7461
+ /**
7462
+ * Read the user's enrollment status. Pending half-enrollments are
7463
+ * reported as `enabled: false` — only a confirmed row counts.
7464
+ */
7465
+ async getStatus(userId) {
7466
+ const row = await this.find(userId);
7467
+ if (!row) return {
7468
+ enabled: false,
7469
+ confirmedAt: null
7470
+ };
7471
+ return {
7472
+ enabled: row.confirmedAt !== null,
7473
+ confirmedAt: row.confirmedAt
7474
+ };
7475
+ }
7476
+ async find(userId) {
7477
+ const results = await this.store.query.query({
7478
+ collection: TOTP_COLLECTION,
7479
+ filter: { where: { userId } }
7480
+ });
7481
+ if (results.length === 0) return null;
7482
+ return parseRow(results[0].data);
7483
+ }
7484
+ };
7485
+ //#endregion
6566
7486
  //#region src/builtins/local-auth/auth-schema.ts
6567
7487
  var USERS_COLLECTION = "users";
6568
7488
  var API_KEYS_COLLECTION = "api_keys";
6569
7489
  var SCOPED_TOKENS_COLLECTION = "scoped_tokens";
7490
+ /**
7491
+ * TOTP enrollment table — split off from `users` to avoid the
7492
+ * destructive drop-+-recreate migration `ensureTable` performs when a
7493
+ * new column is added to a typed schema (see `sqlite-settings-backend.ts`).
7494
+ *
7495
+ * Lifecycle:
7496
+ * - Setup: row inserted with `confirmedAt = null` and a fresh secret.
7497
+ * - Confirm: a valid code from the secret flips `confirmedAt` to now().
7498
+ * - Disable: the row is deleted (next setup starts fresh).
7499
+ *
7500
+ * "Enabled" means `confirmedAt != null`. A half-enrolled row (`confirmedAt
7501
+ * = null`) is INACTIVE — `verifyTotp` returns false until confirmation.
7502
+ */
7503
+ var USER_TOTP_COLLECTION = "user_totp";
6570
7504
  var USERS_COLUMNS = [
6571
7505
  {
6572
7506
  name: "id",
@@ -6586,8 +7520,8 @@ var USERS_COLUMNS = [
6586
7520
  notNull: true
6587
7521
  },
6588
7522
  {
6589
- name: "role",
6590
- type: "TEXT",
7523
+ name: "isAdmin",
7524
+ type: "BOOLEAN",
6591
7525
  notNull: true
6592
7526
  },
6593
7527
  {
@@ -6626,8 +7560,8 @@ var API_KEYS_COLUMNS = [
6626
7560
  notNull: true
6627
7561
  },
6628
7562
  {
6629
- name: "role",
6630
- type: "TEXT",
7563
+ name: "isAdmin",
7564
+ type: "BOOLEAN",
6631
7565
  notNull: true
6632
7566
  },
6633
7567
  {
@@ -6659,6 +7593,28 @@ var API_KEYS_COLUMNS = [
6659
7593
  type: "INTEGER"
6660
7594
  }
6661
7595
  ];
7596
+ var USER_TOTP_COLUMNS = [
7597
+ {
7598
+ name: "userId",
7599
+ type: "TEXT",
7600
+ primaryKey: true,
7601
+ notNull: true
7602
+ },
7603
+ {
7604
+ name: "secret",
7605
+ type: "TEXT",
7606
+ notNull: true
7607
+ },
7608
+ {
7609
+ name: "confirmedAt",
7610
+ type: "INTEGER"
7611
+ },
7612
+ {
7613
+ name: "createdAt",
7614
+ type: "INTEGER",
7615
+ notNull: true
7616
+ }
7617
+ ];
6662
7618
  var SCOPED_TOKENS_COLUMNS = [
6663
7619
  {
6664
7620
  name: "id",
@@ -6732,6 +7688,10 @@ async function declareAuthSchema(store) {
6732
7688
  columns: ["userId"]
6733
7689
  }]
6734
7690
  });
7691
+ await store.declareCollection.mutate({
7692
+ collection: USER_TOTP_COLLECTION,
7693
+ columns: USER_TOTP_COLUMNS
7694
+ });
6735
7695
  }
6736
7696
  //#endregion
6737
7697
  //#region src/builtins/local-auth/local-auth.addon.ts
@@ -6740,7 +7700,7 @@ function toAuthResult(user) {
6740
7700
  userId: user.id,
6741
7701
  username: user.username,
6742
7702
  displayName: user.username,
6743
- roles: [user.role]
7703
+ isAdmin: user.isAdmin
6744
7704
  };
6745
7705
  }
6746
7706
  var LocalAuthAddon = class extends _camstack_types.BaseAddon {
@@ -6748,6 +7708,7 @@ var LocalAuthAddon = class extends _camstack_types.BaseAddon {
6748
7708
  userManager = null;
6749
7709
  apiKeyManager = null;
6750
7710
  scopedTokenManager = null;
7711
+ totpManager = null;
6751
7712
  constructor() {
6752
7713
  super({
6753
7714
  jwtSecret: "",
@@ -6780,6 +7741,7 @@ var LocalAuthAddon = class extends _camstack_types.BaseAddon {
6780
7741
  this.userManager = new UserManager(storageAccess, this.authManager, reader);
6781
7742
  this.apiKeyManager = new ApiKeyManager(storageAccess, this.authManager);
6782
7743
  this.scopedTokenManager = new ScopedTokenManager(store);
7744
+ this.totpManager = new TotpManager(store);
6783
7745
  try {
6784
7746
  await this.userManager.ensureAdminExists();
6785
7747
  const liveUsers = await this.userManager.listAll();
@@ -6895,6 +7857,33 @@ var LocalAuthAddon = class extends _camstack_types.BaseAddon {
6895
7857
  listScopedTokens: async (input) => {
6896
7858
  if (!this.scopedTokenManager) return [];
6897
7859
  return this.scopedTokenManager.listForUser(input.userId);
7860
+ },
7861
+ setupTotp: async (input) => {
7862
+ if (!this.totpManager || !this.userManager) throw new Error("TOTP management not available");
7863
+ const user = await this.userManager.findById(input.userId);
7864
+ if (!user) throw new Error(`User not found: ${input.userId}`);
7865
+ return this.totpManager.setup(user.id, user.username);
7866
+ },
7867
+ confirmTotp: async (input) => {
7868
+ if (!this.totpManager) throw new Error("TOTP management not available");
7869
+ if (!await this.totpManager.confirm(input.userId, input.code)) throw new Error("TOTP confirmation failed — code did not match");
7870
+ return { success: true };
7871
+ },
7872
+ disableTotp: async (input) => {
7873
+ if (!this.totpManager) throw new Error("TOTP management not available");
7874
+ await this.totpManager.disable(input.userId);
7875
+ return { success: true };
7876
+ },
7877
+ getTotpStatus: async (input) => {
7878
+ if (!this.totpManager) return {
7879
+ enabled: false,
7880
+ confirmedAt: null
7881
+ };
7882
+ return this.totpManager.getStatus(input.userId);
7883
+ },
7884
+ verifyTotp: async (input) => {
7885
+ if (!this.totpManager) return { valid: false };
7886
+ return { valid: await this.totpManager.verify(input.userId, input.code) };
6898
7887
  }
6899
7888
  };
6900
7889
  this.ctx.logger.info("registered auth-provider + user-management capabilities");