@camstack/system 1.2.374 → 1.2.376

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 (74) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/auth/external-identity-manager.d.ts +43 -0
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  10. package/dist/builtins/alerts/alerts.addon.js +1 -1
  11. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  12. package/dist/builtins/autotrack/index.js +1 -1
  13. package/dist/builtins/autotrack/index.mjs +1 -1
  14. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  15. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  16. package/dist/builtins/calendars/calendars.addon.js +1 -1
  17. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  18. package/dist/builtins/camera-grid/index.js +1 -1
  19. package/dist/builtins/camera-grid/index.mjs +1 -1
  20. package/dist/builtins/composer/composer.addon.js +2 -2
  21. package/dist/builtins/composer/composer.addon.mjs +2 -2
  22. package/dist/builtins/console-logging/index.js +1 -1
  23. package/dist/builtins/console-logging/index.mjs +1 -1
  24. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  25. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  26. package/dist/builtins/device-manager/device-manager.addon.d.ts +2 -0
  27. package/dist/builtins/device-manager/device-manager.addon.js +36 -2
  28. package/dist/builtins/device-manager/device-manager.addon.mjs +36 -2
  29. package/dist/builtins/device-manager/device-projection.d.ts +2 -1
  30. package/dist/builtins/device-manager/device-provider-context.d.ts +3 -0
  31. package/dist/builtins/device-manager/secret-config-paths.d.ts +24 -0
  32. package/dist/builtins/hub-forwarder/index.js +1 -1
  33. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  34. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  36. package/dist/builtins/local-auth/auth-schema.d.ts +14 -0
  37. package/dist/builtins/local-auth/local-auth.addon.d.ts +1 -0
  38. package/dist/builtins/local-auth/local-auth.addon.js +1343 -1131
  39. package/dist/builtins/local-auth/local-auth.addon.mjs +1343 -1131
  40. package/dist/builtins/local-network/local-network.addon.js +1 -1
  41. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  42. package/dist/builtins/loki-logging/index.js +1 -1
  43. package/dist/builtins/loki-logging/index.mjs +1 -1
  44. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  45. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  46. package/dist/builtins/platform-probe/index.js +1 -1
  47. package/dist/builtins/platform-probe/index.mjs +1 -1
  48. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  49. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  50. package/dist/builtins/snapshot/index.js +1 -1
  51. package/dist/builtins/snapshot/index.mjs +1 -1
  52. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  53. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  54. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  55. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  56. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  57. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  58. package/dist/builtins/system-config/system-config.addon.js +1 -1
  59. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  60. package/dist/builtins/winston-logging/index.js +1 -1
  61. package/dist/builtins/winston-logging/index.mjs +1 -1
  62. package/dist/{child-cap-dispatch-B9kziY4L.mjs → child-cap-dispatch-8qMgog1O.mjs} +16 -4
  63. package/dist/{child-cap-dispatch-Bfdgkbq3.js → child-cap-dispatch-BWtaSmWf.js} +15 -3
  64. package/dist/{composition-sources-DYlXX2qW.js → composition-sources-DW7CgMsg.js} +1 -1
  65. package/dist/{composition-sources-BfZgiTYD.mjs → composition-sources-zxQPPtP_.mjs} +1 -1
  66. package/dist/{dist-CNbnikIh.js → dist-BcUWQGr9.js} +268 -6
  67. package/dist/{dist-ZdSg09GJ.mjs → dist-D732NygW.mjs} +263 -7
  68. package/dist/{event-loop-stall-monitor-DKcYBHdH.mjs → event-loop-stall-monitor-8W4yWNVW.mjs} +1 -1
  69. package/dist/{event-loop-stall-monitor-CpEH3CoV.js → event-loop-stall-monitor-QX4RjNe7.js} +1 -1
  70. package/dist/index.js +3 -3
  71. package/dist/index.mjs +3 -3
  72. package/dist/{retired-settings-keys-B4ZeKrbR.mjs → retired-settings-keys-CxPOg7BF.mjs} +1 -1
  73. package/dist/{retired-settings-keys-CcWvdy6u.js → retired-settings-keys-MaESpO18.js} +1 -1
  74. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CNbnikIh.js");
6
+ const require_dist = require("../../dist-BcUWQGr9.js");
7
7
  let zod = require("zod");
8
8
  let node_crypto = require("node:crypto");
9
9
  node_crypto = require_chunk.__toESM(node_crypto);
@@ -6334,7 +6334,9 @@ var AuthManager = class {
6334
6334
  ...payload.integrationId !== void 0 ? { integrationId: payload.integrationId } : {},
6335
6335
  ...payload.jti !== void 0 ? { jti: payload.jti } : {},
6336
6336
  ...payload.sessionId !== void 0 ? { sessionId: payload.sessionId } : {},
6337
- ...payload.refreshTtl !== void 0 ? { refreshTtl: payload.refreshTtl } : {}
6337
+ ...payload.refreshTtl !== void 0 ? { refreshTtl: payload.refreshTtl } : {},
6338
+ ...payload.linkIntent !== void 0 ? { linkIntent: payload.linkIntent } : {},
6339
+ ...payload.issuer !== void 0 ? { issuer: payload.issuer } : {}
6338
6340
  };
6339
6341
  /**
6340
6342
  * `'never'` mints with NO `exp` claim, and the option must be OMITTED to do
@@ -6376,6 +6378,8 @@ var AuthManager = class {
6376
6378
  const integrationId = typeof decoded["integrationId"] === "string" ? decoded["integrationId"] : void 0;
6377
6379
  const jti = typeof decoded["jti"] === "string" ? decoded["jti"] : void 0;
6378
6380
  const sessionId = typeof decoded["sessionId"] === "string" ? decoded["sessionId"] : void 0;
6381
+ const linkIntent = typeof decoded["linkIntent"] === "string" ? decoded["linkIntent"] : void 0;
6382
+ const issuer = typeof decoded["issuer"] === "string" ? decoded["issuer"] : void 0;
6379
6383
  const rawRefreshTtl = decoded["refreshTtl"];
6380
6384
  const refreshTtl = rawRefreshTtl === "never" || typeof rawRefreshTtl === "number" && rawRefreshTtl > 0 ? rawRefreshTtl : void 0;
6381
6385
  return {
@@ -6391,7 +6395,9 @@ var AuthManager = class {
6391
6395
  ...integrationId !== void 0 ? { integrationId } : {},
6392
6396
  ...jti !== void 0 ? { jti } : {},
6393
6397
  ...sessionId !== void 0 ? { sessionId } : {},
6394
- ...refreshTtl !== void 0 ? { refreshTtl } : {}
6398
+ ...refreshTtl !== void 0 ? { refreshTtl } : {},
6399
+ ...linkIntent !== void 0 ? { linkIntent } : {},
6400
+ ...issuer !== void 0 ? { issuer } : {}
6395
6401
  };
6396
6402
  } catch {
6397
6403
  return null;
@@ -6772,168 +6778,652 @@ var UserManager = class {
6772
6778
  if (!user) return false;
6773
6779
  return this.auth.comparePassword(password, user.passwordHash);
6774
6780
  }
6775
- }, i$2 = class extends Error {
6776
- constructor(e, t) {
6777
- super(e, t), this.name = "OTPError";
6778
- }
6779
- }, G$3 = class extends i$2 {
6780
- constructor(e) {
6781
- super(e), this.name = "PluginError";
6782
- }
6783
- }, Y$3 = class extends G$3 {
6784
- constructor() {
6785
- super("Crypto plugin is required."), this.name = "CryptoPluginMissingError";
6786
- }
6787
- }, L$2 = class extends G$3 {
6788
- constructor() {
6789
- super("Base32 plugin is required."), this.name = "Base32PluginMissingError";
6790
- }
6791
- }, a$2 = class extends i$2 {
6792
- constructor(e) {
6793
- super(e), this.name = "ConfigurationError";
6794
- }
6795
6781
  };
6796
- new TextEncoder();
6797
- new TextDecoder();
6798
- var ur$3 = 3600, pr$3 = Number.MAX_SAFE_INTEGER, er$3 = Symbol("otplib.guardrails.override");
6799
- function y$2(r, e, t) {
6800
- if (typeof e != "number" || !Number.isSafeInteger(e)) throw new a$2(`Guardrail '${r}' must be a safe integer`);
6801
- if (e < t) throw new a$2(`Guardrail '${r}' must be >= ${t}`);
6802
- }
6803
- var d$5 = Object.freeze({
6804
- MIN_SECRET_BYTES: 16,
6805
- MAX_SECRET_BYTES: 64,
6806
- MIN_PERIOD: 1,
6807
- MAX_PERIOD: ur$3,
6808
- MAX_COUNTER: pr$3,
6809
- MAX_WINDOW: 99,
6810
- [er$3]: !1
6811
- });
6812
- function hr$2(r) {
6813
- if (!r) return d$5;
6814
- r.MIN_SECRET_BYTES !== void 0 && y$2("MIN_SECRET_BYTES", r.MIN_SECRET_BYTES, 1), r.MAX_SECRET_BYTES !== void 0 && y$2("MAX_SECRET_BYTES", r.MAX_SECRET_BYTES, 1), r.MIN_PERIOD !== void 0 && y$2("MIN_PERIOD", r.MIN_PERIOD, 1), r.MAX_PERIOD !== void 0 && y$2("MAX_PERIOD", r.MAX_PERIOD, 1), r.MAX_COUNTER !== void 0 && y$2("MAX_COUNTER", r.MAX_COUNTER, 0), r.MAX_WINDOW !== void 0 && y$2("MAX_WINDOW", r.MAX_WINDOW, 1);
6815
- let e = {
6816
- ...d$5,
6817
- ...r
6818
- };
6819
- if (e.MIN_SECRET_BYTES > e.MAX_SECRET_BYTES) throw new a$2("Guardrail 'MIN_SECRET_BYTES' must be <= 'MAX_SECRET_BYTES'");
6820
- if (e.MIN_PERIOD > e.MAX_PERIOD) throw new a$2("Guardrail 'MIN_PERIOD' must be <= 'MAX_PERIOD'");
6821
- return Object.freeze({
6822
- ...e,
6823
- [er$3]: !0
6824
- });
6825
- }
6826
- function Dr(r) {
6827
- let { crypto: e, base32: t, length: o = 20 } = r;
6828
- dr$2(e), nr$2(t);
6829
- let n = e.randomBytes(o);
6830
- return t.encode(n, { padding: !1 });
6831
- }
6832
- function dr$2(r) {
6833
- if (!r) throw new Y$3();
6834
- }
6835
- function nr$2(r) {
6836
- if (!r) throw new L$2();
6837
- }
6838
6782
  //#endregion
6839
- //#region ../../node_modules/@otplib/hotp/node_modules/@otplib/core/dist/index.js
6840
- var i$1 = class extends Error {
6841
- constructor(e, t) {
6842
- super(e, t), this.name = "OTPError";
6843
- }
6844
- }, x$2 = class extends i$1 {
6845
- constructor(e) {
6846
- super(e), this.name = "SecretError";
6847
- }
6848
- }, h$1 = class extends x$2 {
6849
- constructor(e, t) {
6850
- super(`Secret must be at least ${e} bytes (${e * 8} bits), got ${t} bytes`), this.name = "SecretTooShortError";
6783
+ //#region src/builtins/local-auth/auth-schema.ts
6784
+ /**
6785
+ * @durable class=config owner=local-auth
6786
+ * write="an operator creates a user, or `ensureAdminExists` seeds the configured
6787
+ * admin on first boot (or resets its password on the operator's one-shot
6788
+ * RESET_ADMIN_PASSWORD trigger); updated on scope, isAdmin and password edits"
6789
+ * retention="none — a row goes only when an operator deletes the user. A
6790
+ * `declareCollection` that adds a PK/UNIQUE/NOT-NULL-without-default column
6791
+ * REBUILDS the table: every account is wiped and only the configured admin
6792
+ * comes back on the next boot."
6793
+ */
6794
+ var USERS_COLLECTION = "users";
6795
+ /**
6796
+ * @durable class=config owner=local-auth
6797
+ * write="an operator mints an API key; `validateToken` rewrites `lastUsedAt` on
6798
+ * every request the key authorises"
6799
+ * retention="none — an explicit `revoke` is the only delete. API keys have no
6800
+ * expiry column and no sweep, so a key lives until someone removes it (or the
6801
+ * destructive `declareCollection` rebuild takes the table)."
6802
+ */
6803
+ var API_KEYS_COLLECTION = "api_keys";
6804
+ /**
6805
+ * @durable class=config owner=local-auth
6806
+ * write="an operator or the CLI mints a scoped token; `validate` fires
6807
+ * `updateLastUsed` on every accepted call"
6808
+ * retention="`revoke` deletes one. The daily sweep `reapExpired` drops rows past
6809
+ * `expiresAt` — a NULL expiry is a deliberate never-expires token and BETWEEN
6810
+ * never matches it. `cleanupOrphans` drops tokens owned by the legacy 'system'
6811
+ * id or by a user that no longer exists."
6812
+ */
6813
+ var SCOPED_TOKENS_COLLECTION = "scoped_tokens";
6814
+ /**
6815
+ * @durable class=ledger owner=local-auth
6816
+ * write="one row per redeemed OAuth code — every completed account link through
6817
+ * `oauthExchangeCode`; `touch` rewrites `lastUsedAt` on each refresh"
6818
+ * retention="ONLY revoked rows are pruned: the daily sweep deletes sessions
6819
+ * revoked more than OAUTH_REVOKED_RETENTION_MS (30 d) ago. An ACTIVE session
6820
+ * has `revokedAt` NULL, never expires and is never swept — the table grows one
6821
+ * row per login until an operator revokes the link."
6822
+ */
6823
+ var OAUTH_SESSIONS_COLLECTION = "oauth_sessions";
6824
+ /**
6825
+ * TOTP enrollment table — split off from `users` to avoid the
6826
+ * destructive drop-+-recreate migration `ensureTable` performs when a
6827
+ * new column is added to a typed schema (see `sqlite-settings-backend.ts`).
6828
+ *
6829
+ * Lifecycle:
6830
+ * - Setup: row inserted with `confirmedAt = null` and a fresh secret.
6831
+ * - Confirm: a valid code from the secret flips `confirmedAt` to now().
6832
+ * - Disable: the row is deleted (next setup starts fresh).
6833
+ *
6834
+ * "Enabled" means `confirmedAt != null`. A half-enrolled row (`confirmedAt
6835
+ * = null`) is INACTIVE — `verifyTotp` returns false until confirmation.
6836
+ *
6837
+ * @durable class=config owner=local-auth
6838
+ * write="`setup` inserts a fresh secret for the user (deleting any pending row
6839
+ * first); `confirm` flips `confirmedAt` once a valid code proves possession"
6840
+ * retention="`disable` deletes the row, and `setup` deletes before re-inserting.
6841
+ * Nothing else prunes — deleting a USER does not cascade here, so their
6842
+ * enrollment row outlives the account."
6843
+ */
6844
+ var USER_TOTP_COLLECTION = "user_totp";
6845
+ /**
6846
+ * External sign-in identities linked to local users (D782): one row per
6847
+ * `(provider, subject)` — the row id IS the identity, so one identity maps to
6848
+ * one user by primary key. Written only by the identity's owner completing a
6849
+ * "Link" flow from their profile (bound to their session).
6850
+ *
6851
+ * @durable class=config owner=local-auth
6852
+ * write="`ExternalIdentityManager.link` when a signed-in user completes a
6853
+ * link flow; `unlink` from the profile or the Users page"
6854
+ * retention="`unlink` deletes one row; deleting the user deletes all of its
6855
+ * rows (`deleteUser` → `removeAllForUser`). A row whose provider instance
6856
+ * was renamed or removed stays, inert, until someone unlinks it."
6857
+ */
6858
+ var USER_EXTERNAL_IDENTITIES_COLLECTION = "user_external_identities";
6859
+ var USERS_COLUMNS = [
6860
+ {
6861
+ name: "id",
6862
+ type: "TEXT",
6863
+ primaryKey: true,
6864
+ notNull: true
6865
+ },
6866
+ {
6867
+ name: "username",
6868
+ type: "TEXT",
6869
+ notNull: true,
6870
+ unique: true
6871
+ },
6872
+ {
6873
+ name: "passwordHash",
6874
+ type: "TEXT",
6875
+ notNull: true
6876
+ },
6877
+ {
6878
+ name: "isAdmin",
6879
+ type: "BOOLEAN",
6880
+ notNull: true
6881
+ },
6882
+ {
6883
+ name: "allowedProviders",
6884
+ type: "JSON"
6885
+ },
6886
+ {
6887
+ name: "allowedDevices",
6888
+ type: "JSON"
6889
+ },
6890
+ {
6891
+ name: "scopes",
6892
+ type: "JSON"
6893
+ },
6894
+ {
6895
+ name: "createdAt",
6896
+ type: "INTEGER",
6897
+ notNull: true
6898
+ },
6899
+ {
6900
+ name: "updatedAt",
6901
+ type: "INTEGER",
6902
+ notNull: true
6851
6903
  }
6852
- }, P$1 = class extends x$2 {
6853
- constructor(e, t) {
6854
- super(`Secret must not exceed ${e} bytes, got ${t} bytes`), this.name = "SecretTooLongError";
6904
+ ];
6905
+ var API_KEYS_COLUMNS = [
6906
+ {
6907
+ name: "id",
6908
+ type: "TEXT",
6909
+ primaryKey: true,
6910
+ notNull: true
6911
+ },
6912
+ {
6913
+ name: "label",
6914
+ type: "TEXT",
6915
+ notNull: true
6916
+ },
6917
+ {
6918
+ name: "isAdmin",
6919
+ type: "BOOLEAN",
6920
+ notNull: true
6921
+ },
6922
+ {
6923
+ name: "tokenHash",
6924
+ type: "TEXT",
6925
+ notNull: true,
6926
+ unique: true
6927
+ },
6928
+ {
6929
+ name: "tokenPrefix",
6930
+ type: "TEXT",
6931
+ notNull: true
6932
+ },
6933
+ {
6934
+ name: "allowedProviders",
6935
+ type: "JSON"
6936
+ },
6937
+ {
6938
+ name: "allowedDevices",
6939
+ type: "JSON"
6940
+ },
6941
+ {
6942
+ name: "createdAt",
6943
+ type: "INTEGER",
6944
+ notNull: true
6945
+ },
6946
+ {
6947
+ name: "lastUsedAt",
6948
+ type: "INTEGER"
6855
6949
  }
6856
- }, m$2 = class extends i$1 {
6857
- constructor(e) {
6858
- super(e), this.name = "CounterError";
6950
+ ];
6951
+ var USER_TOTP_COLUMNS = [
6952
+ {
6953
+ name: "userId",
6954
+ type: "TEXT",
6955
+ primaryKey: true,
6956
+ notNull: true
6957
+ },
6958
+ {
6959
+ name: "secret",
6960
+ type: "TEXT",
6961
+ notNull: true
6962
+ },
6963
+ {
6964
+ name: "confirmedAt",
6965
+ type: "INTEGER"
6966
+ },
6967
+ {
6968
+ name: "createdAt",
6969
+ type: "INTEGER",
6970
+ notNull: true
6859
6971
  }
6860
- }, O$2 = class extends m$2 {
6861
- constructor() {
6862
- super("Counter must be non-negative"), this.name = "CounterNegativeError";
6972
+ ];
6973
+ var USER_EXTERNAL_IDENTITIES_COLUMNS = [
6974
+ {
6975
+ name: "id",
6976
+ type: "TEXT",
6977
+ primaryKey: true,
6978
+ notNull: true
6979
+ },
6980
+ {
6981
+ name: "provider",
6982
+ type: "TEXT",
6983
+ notNull: true
6984
+ },
6985
+ {
6986
+ name: "subject",
6987
+ type: "TEXT",
6988
+ notNull: true
6989
+ },
6990
+ {
6991
+ name: "issuer",
6992
+ type: "TEXT",
6993
+ notNull: true
6994
+ },
6995
+ {
6996
+ name: "userId",
6997
+ type: "TEXT",
6998
+ notNull: true
6999
+ },
7000
+ {
7001
+ name: "label",
7002
+ type: "TEXT"
7003
+ },
7004
+ {
7005
+ name: "linkedAt",
7006
+ type: "INTEGER",
7007
+ notNull: true
6863
7008
  }
6864
- }, T$2 = class extends m$2 {
6865
- constructor() {
6866
- super("Counter exceeds maximum safe integer value"), this.name = "CounterOverflowError";
7009
+ ];
7010
+ var SCOPED_TOKENS_COLUMNS = [
7011
+ {
7012
+ name: "id",
7013
+ type: "TEXT",
7014
+ primaryKey: true,
7015
+ notNull: true
7016
+ },
7017
+ {
7018
+ name: "userId",
7019
+ type: "TEXT",
7020
+ notNull: true
7021
+ },
7022
+ {
7023
+ name: "name",
7024
+ type: "TEXT",
7025
+ notNull: true
7026
+ },
7027
+ {
7028
+ name: "tokenHash",
7029
+ type: "TEXT",
7030
+ notNull: true,
7031
+ unique: true
7032
+ },
7033
+ {
7034
+ name: "tokenPrefix",
7035
+ type: "TEXT",
7036
+ notNull: true
7037
+ },
7038
+ {
7039
+ name: "scopes",
7040
+ type: "JSON"
7041
+ },
7042
+ {
7043
+ name: "expiresAt",
7044
+ type: "INTEGER"
7045
+ },
7046
+ {
7047
+ name: "lastUsedAt",
7048
+ type: "INTEGER"
7049
+ },
7050
+ {
7051
+ name: "createdAt",
7052
+ type: "INTEGER",
7053
+ notNull: true
6867
7054
  }
6868
- }, b$2 = class extends m$2 {
7055
+ ];
7056
+ var OAUTH_SESSIONS_COLUMNS = [
7057
+ {
7058
+ name: "id",
7059
+ type: "TEXT",
7060
+ primaryKey: true,
7061
+ notNull: true
7062
+ },
7063
+ {
7064
+ name: "userId",
7065
+ type: "TEXT",
7066
+ notNull: true
7067
+ },
7068
+ {
7069
+ name: "username",
7070
+ type: "TEXT",
7071
+ notNull: true
7072
+ },
7073
+ {
7074
+ name: "integrationId",
7075
+ type: "TEXT",
7076
+ notNull: true
7077
+ },
7078
+ {
7079
+ name: "scopes",
7080
+ type: "JSON"
7081
+ },
7082
+ {
7083
+ name: "createdAt",
7084
+ type: "INTEGER",
7085
+ notNull: true
7086
+ },
7087
+ {
7088
+ name: "lastUsedAt",
7089
+ type: "INTEGER",
7090
+ notNull: true
7091
+ },
7092
+ {
7093
+ name: "revokedAt",
7094
+ type: "INTEGER"
7095
+ }
7096
+ ];
7097
+ /**
7098
+ * Declare every auth collection with the typed schema. Called by
7099
+ * `LocalAuthAddon.onInitialize` before constructing the per-collection
7100
+ * managers. Idempotent — `declareCollection` is a no-op when the
7101
+ * collection has already been declared (same shape).
7102
+ */
7103
+ async function declareAuthSchema(store) {
7104
+ await store.declareCollection.mutate({
7105
+ collection: USERS_COLLECTION,
7106
+ columns: USERS_COLUMNS
7107
+ });
7108
+ await store.declareCollection.mutate({
7109
+ collection: API_KEYS_COLLECTION,
7110
+ columns: API_KEYS_COLUMNS,
7111
+ indexes: [{
7112
+ name: "idx_api_keys_token_prefix",
7113
+ columns: ["tokenPrefix"]
7114
+ }]
7115
+ });
7116
+ await store.declareCollection.mutate({
7117
+ collection: SCOPED_TOKENS_COLLECTION,
7118
+ columns: SCOPED_TOKENS_COLUMNS,
7119
+ indexes: [{
7120
+ name: "idx_scoped_tokens_user_id",
7121
+ columns: ["userId"]
7122
+ }]
7123
+ });
7124
+ await store.declareCollection.mutate({
7125
+ collection: USER_TOTP_COLLECTION,
7126
+ columns: USER_TOTP_COLUMNS
7127
+ });
7128
+ await store.declareCollection.mutate({
7129
+ collection: USER_EXTERNAL_IDENTITIES_COLLECTION,
7130
+ columns: USER_EXTERNAL_IDENTITIES_COLUMNS,
7131
+ indexes: [{
7132
+ name: "idx_user_external_identities_user_id",
7133
+ columns: ["userId"]
7134
+ }]
7135
+ });
7136
+ await store.declareCollection.mutate({
7137
+ collection: OAUTH_SESSIONS_COLLECTION,
7138
+ columns: OAUTH_SESSIONS_COLUMNS,
7139
+ indexes: [{
7140
+ name: "idx_oauth_sessions_user_id",
7141
+ columns: ["userId"]
7142
+ }]
7143
+ });
7144
+ }
7145
+ //#endregion
7146
+ //#region src/auth/external-identity-manager.ts
7147
+ /** The identity is already linked to a different user. */
7148
+ var ExternalIdentityConflictError = class extends Error {
6869
7149
  constructor() {
6870
- super("Counter must be a finite integer"), this.name = "CounterNotIntegerError";
7150
+ super("This sign-in account is already linked to another CamStack user");
7151
+ this.name = "ExternalIdentityConflictError";
6871
7152
  }
6872
- }, R$2 = class extends i$1 {
6873
- constructor(e) {
6874
- super(e), this.name = "TokenError";
7153
+ };
7154
+ function rowId(key) {
7155
+ return JSON.stringify([key.provider, key.subject]);
7156
+ }
7157
+ function parseRow$1(data) {
7158
+ const { provider, subject, issuer, userId, label, linkedAt } = data;
7159
+ if (typeof provider !== "string" || typeof subject !== "string") return null;
7160
+ if (typeof issuer !== "string") return null;
7161
+ if (typeof userId !== "string" || typeof linkedAt !== "number") return null;
7162
+ return {
7163
+ provider,
7164
+ subject,
7165
+ issuer,
7166
+ userId,
7167
+ linkedAt,
7168
+ ...typeof label === "string" && label.length > 0 ? { label } : {}
7169
+ };
7170
+ }
7171
+ var ExternalIdentityManager = class {
7172
+ store;
7173
+ constructor(store) {
7174
+ this.store = store;
6875
7175
  }
6876
- }, I$2 = class extends R$2 {
6877
- constructor(e, t) {
6878
- super(`Token must be ${e} digits, got ${t}`), this.name = "TokenLengthError";
7176
+ async find(key) {
7177
+ const rows = await this.store.query.query({
7178
+ collection: USER_EXTERNAL_IDENTITIES_COLLECTION,
7179
+ filter: { where: {
7180
+ provider: key.provider,
7181
+ subject: key.subject
7182
+ } }
7183
+ });
7184
+ for (const row of rows) {
7185
+ const parsed = parseRow$1(row.data);
7186
+ if (parsed !== null) return parsed;
7187
+ }
7188
+ return null;
6879
7189
  }
6880
- }, M$1 = class extends R$2 {
6881
- constructor() {
6882
- super("Token must contain only digits"), this.name = "TokenFormatError";
7190
+ /** The link of `(provider, subject)`, or `null`. */
7191
+ async findLink(provider, subject) {
7192
+ return this.find({
7193
+ provider,
7194
+ subject
7195
+ });
6883
7196
  }
6884
- }, N$1 = class extends i$1 {
6885
- constructor(e, t) {
6886
- super(e, t), this.name = "CryptoError";
7197
+ /** The user `(provider, subject)` is linked to, or `null`. */
7198
+ async findUser(provider, subject) {
7199
+ return (await this.find({
7200
+ provider,
7201
+ subject
7202
+ }))?.userId ?? null;
6887
7203
  }
6888
- }, c$2 = class extends N$1 {
6889
- constructor(e, t) {
6890
- super(`HMAC computation failed: ${e}`, t), this.name = "HMACError";
7204
+ async listForUser(userId) {
7205
+ return (await this.store.query.query({
7206
+ collection: USER_EXTERNAL_IDENTITIES_COLLECTION,
7207
+ filter: { where: { userId } }
7208
+ })).flatMap((row) => {
7209
+ const parsed = parseRow$1(row.data);
7210
+ return parsed === null ? [] : [parsed];
7211
+ });
6891
7212
  }
6892
- }, v$2 = class extends N$1 {
6893
- constructor(e, t) {
6894
- super(`Random byte generation failed: ${e}`, t), this.name = "RandomBytesError";
7213
+ /**
7214
+ * Link `(provider, subject)` to `userId`. Idempotent for the same user;
7215
+ * {@link ExternalIdentityConflictError} when another user holds it.
7216
+ */
7217
+ async link(input) {
7218
+ const existing = await this.find(input);
7219
+ if (existing !== null) {
7220
+ if (existing.userId === input.userId) return;
7221
+ throw new ExternalIdentityConflictError();
7222
+ }
7223
+ const record = {
7224
+ provider: input.provider,
7225
+ subject: input.subject,
7226
+ issuer: input.issuer,
7227
+ userId: input.userId,
7228
+ linkedAt: Date.now(),
7229
+ ...input.label !== void 0 && input.label.length > 0 ? { label: input.label } : {}
7230
+ };
7231
+ try {
7232
+ await this.store.insert.mutate({
7233
+ collection: USER_EXTERNAL_IDENTITIES_COLLECTION,
7234
+ record: {
7235
+ id: rowId(input),
7236
+ data: { ...record }
7237
+ }
7238
+ });
7239
+ } catch (err) {
7240
+ const winner = await this.find(input);
7241
+ if (winner?.userId === input.userId) return;
7242
+ if (winner !== null) throw new ExternalIdentityConflictError();
7243
+ throw err;
7244
+ }
6895
7245
  }
6896
- }, g$1 = class extends i$1 {
6897
- constructor(e) {
6898
- super(e), this.name = "CounterToleranceError";
7246
+ /** Remove one link of `userId`; `false` when that user did not hold it. */
7247
+ async unlink(input) {
7248
+ const existing = await this.find(input);
7249
+ if (existing === null || existing.userId !== input.userId) return false;
7250
+ await this.store.delete.mutate({
7251
+ collection: USER_EXTERNAL_IDENTITIES_COLLECTION,
7252
+ key: rowId(input)
7253
+ });
7254
+ return true;
6899
7255
  }
6900
- }, U$2 = class extends g$1 {
6901
- constructor(e, t) {
6902
- super(`Counter tolerance validation failed: total checks (${t}) exceeds MAX_WINDOW (${e})`), this.name = "CounterToleranceTooLargeError";
7256
+ /** Delete every link of a user being deleted. Returns how many were removed. */
7257
+ async removeAllForUser(userId) {
7258
+ const links = await this.listForUser(userId);
7259
+ for (const link of links) await this.store.delete.mutate({
7260
+ collection: USER_EXTERNAL_IDENTITIES_COLLECTION,
7261
+ key: rowId(link)
7262
+ });
7263
+ return links.length;
6903
7264
  }
6904
- }, X$1 = class extends g$1 {
6905
- constructor() {
6906
- super("Counter tolerance cannot contain negative values"), this.name = "CounterToleranceNegativeError";
7265
+ }, i$2 = class extends Error {
7266
+ constructor(e, t) {
7267
+ super(e, t), this.name = "OTPError";
6907
7268
  }
6908
- }, G$2 = class extends i$1 {
7269
+ }, G$3 = class extends i$2 {
6909
7270
  constructor(e) {
6910
7271
  super(e), this.name = "PluginError";
6911
7272
  }
6912
- }, Y$2 = class extends G$2 {
7273
+ }, Y$3 = class extends G$3 {
6913
7274
  constructor() {
6914
7275
  super("Crypto plugin is required."), this.name = "CryptoPluginMissingError";
6915
7276
  }
6916
- }, L$1 = class extends G$2 {
7277
+ }, L$2 = class extends G$3 {
6917
7278
  constructor() {
6918
7279
  super("Base32 plugin is required."), this.name = "Base32PluginMissingError";
6919
7280
  }
6920
- }, a$1 = class extends i$1 {
7281
+ }, a$2 = class extends i$2 {
6921
7282
  constructor(e) {
6922
7283
  super(e), this.name = "ConfigurationError";
6923
7284
  }
6924
- }, W$1 = class extends a$1 {
6925
- constructor() {
6926
- super("Secret is required. Use generateSecret() to create one, or provide via { secret: 'YOUR_BASE32_SECRET' }"), this.name = "SecretMissingError";
6927
- }
6928
7285
  };
6929
7286
  new TextEncoder();
6930
7287
  new TextDecoder();
6931
- var ur$2 = 3600, pr$2 = Number.MAX_SAFE_INTEGER, er$2 = Symbol("otplib.guardrails.override");
6932
- function y$1(r, e, t) {
6933
- if (typeof e != "number" || !Number.isSafeInteger(e)) throw new a$1(`Guardrail '${r}' must be a safe integer`);
6934
- if (e < t) throw new a$1(`Guardrail '${r}' must be >= ${t}`);
7288
+ var ur$3 = 3600, pr$3 = Number.MAX_SAFE_INTEGER, er$3 = Symbol("otplib.guardrails.override");
7289
+ function y$2(r, e, t) {
7290
+ if (typeof e != "number" || !Number.isSafeInteger(e)) throw new a$2(`Guardrail '${r}' must be a safe integer`);
7291
+ if (e < t) throw new a$2(`Guardrail '${r}' must be >= ${t}`);
6935
7292
  }
6936
- var d$4 = Object.freeze({
7293
+ var d$5 = Object.freeze({
7294
+ MIN_SECRET_BYTES: 16,
7295
+ MAX_SECRET_BYTES: 64,
7296
+ MIN_PERIOD: 1,
7297
+ MAX_PERIOD: ur$3,
7298
+ MAX_COUNTER: pr$3,
7299
+ MAX_WINDOW: 99,
7300
+ [er$3]: !1
7301
+ });
7302
+ function hr$2(r) {
7303
+ if (!r) return d$5;
7304
+ r.MIN_SECRET_BYTES !== void 0 && y$2("MIN_SECRET_BYTES", r.MIN_SECRET_BYTES, 1), r.MAX_SECRET_BYTES !== void 0 && y$2("MAX_SECRET_BYTES", r.MAX_SECRET_BYTES, 1), r.MIN_PERIOD !== void 0 && y$2("MIN_PERIOD", r.MIN_PERIOD, 1), r.MAX_PERIOD !== void 0 && y$2("MAX_PERIOD", r.MAX_PERIOD, 1), r.MAX_COUNTER !== void 0 && y$2("MAX_COUNTER", r.MAX_COUNTER, 0), r.MAX_WINDOW !== void 0 && y$2("MAX_WINDOW", r.MAX_WINDOW, 1);
7305
+ let e = {
7306
+ ...d$5,
7307
+ ...r
7308
+ };
7309
+ if (e.MIN_SECRET_BYTES > e.MAX_SECRET_BYTES) throw new a$2("Guardrail 'MIN_SECRET_BYTES' must be <= 'MAX_SECRET_BYTES'");
7310
+ if (e.MIN_PERIOD > e.MAX_PERIOD) throw new a$2("Guardrail 'MIN_PERIOD' must be <= 'MAX_PERIOD'");
7311
+ return Object.freeze({
7312
+ ...e,
7313
+ [er$3]: !0
7314
+ });
7315
+ }
7316
+ function Dr(r) {
7317
+ let { crypto: e, base32: t, length: o = 20 } = r;
7318
+ dr$2(e), nr$2(t);
7319
+ let n = e.randomBytes(o);
7320
+ return t.encode(n, { padding: !1 });
7321
+ }
7322
+ function dr$2(r) {
7323
+ if (!r) throw new Y$3();
7324
+ }
7325
+ function nr$2(r) {
7326
+ if (!r) throw new L$2();
7327
+ }
7328
+ //#endregion
7329
+ //#region ../../node_modules/@otplib/hotp/node_modules/@otplib/core/dist/index.js
7330
+ var i$1 = class extends Error {
7331
+ constructor(e, t) {
7332
+ super(e, t), this.name = "OTPError";
7333
+ }
7334
+ }, x$2 = class extends i$1 {
7335
+ constructor(e) {
7336
+ super(e), this.name = "SecretError";
7337
+ }
7338
+ }, h$1 = class extends x$2 {
7339
+ constructor(e, t) {
7340
+ super(`Secret must be at least ${e} bytes (${e * 8} bits), got ${t} bytes`), this.name = "SecretTooShortError";
7341
+ }
7342
+ }, P$1 = class extends x$2 {
7343
+ constructor(e, t) {
7344
+ super(`Secret must not exceed ${e} bytes, got ${t} bytes`), this.name = "SecretTooLongError";
7345
+ }
7346
+ }, m$2 = class extends i$1 {
7347
+ constructor(e) {
7348
+ super(e), this.name = "CounterError";
7349
+ }
7350
+ }, O$2 = class extends m$2 {
7351
+ constructor() {
7352
+ super("Counter must be non-negative"), this.name = "CounterNegativeError";
7353
+ }
7354
+ }, T$2 = class extends m$2 {
7355
+ constructor() {
7356
+ super("Counter exceeds maximum safe integer value"), this.name = "CounterOverflowError";
7357
+ }
7358
+ }, b$2 = class extends m$2 {
7359
+ constructor() {
7360
+ super("Counter must be a finite integer"), this.name = "CounterNotIntegerError";
7361
+ }
7362
+ }, R$2 = class extends i$1 {
7363
+ constructor(e) {
7364
+ super(e), this.name = "TokenError";
7365
+ }
7366
+ }, I$2 = class extends R$2 {
7367
+ constructor(e, t) {
7368
+ super(`Token must be ${e} digits, got ${t}`), this.name = "TokenLengthError";
7369
+ }
7370
+ }, M$1 = class extends R$2 {
7371
+ constructor() {
7372
+ super("Token must contain only digits"), this.name = "TokenFormatError";
7373
+ }
7374
+ }, N$1 = class extends i$1 {
7375
+ constructor(e, t) {
7376
+ super(e, t), this.name = "CryptoError";
7377
+ }
7378
+ }, c$2 = class extends N$1 {
7379
+ constructor(e, t) {
7380
+ super(`HMAC computation failed: ${e}`, t), this.name = "HMACError";
7381
+ }
7382
+ }, v$2 = class extends N$1 {
7383
+ constructor(e, t) {
7384
+ super(`Random byte generation failed: ${e}`, t), this.name = "RandomBytesError";
7385
+ }
7386
+ }, g$1 = class extends i$1 {
7387
+ constructor(e) {
7388
+ super(e), this.name = "CounterToleranceError";
7389
+ }
7390
+ }, U$2 = class extends g$1 {
7391
+ constructor(e, t) {
7392
+ super(`Counter tolerance validation failed: total checks (${t}) exceeds MAX_WINDOW (${e})`), this.name = "CounterToleranceTooLargeError";
7393
+ }
7394
+ }, X$1 = class extends g$1 {
7395
+ constructor() {
7396
+ super("Counter tolerance cannot contain negative values"), this.name = "CounterToleranceNegativeError";
7397
+ }
7398
+ }, G$2 = class extends i$1 {
7399
+ constructor(e) {
7400
+ super(e), this.name = "PluginError";
7401
+ }
7402
+ }, Y$2 = class extends G$2 {
7403
+ constructor() {
7404
+ super("Crypto plugin is required."), this.name = "CryptoPluginMissingError";
7405
+ }
7406
+ }, L$1 = class extends G$2 {
7407
+ constructor() {
7408
+ super("Base32 plugin is required."), this.name = "Base32PluginMissingError";
7409
+ }
7410
+ }, a$1 = class extends i$1 {
7411
+ constructor(e) {
7412
+ super(e), this.name = "ConfigurationError";
7413
+ }
7414
+ }, W$1 = class extends a$1 {
7415
+ constructor() {
7416
+ super("Secret is required. Use generateSecret() to create one, or provide via { secret: 'YOUR_BASE32_SECRET' }"), this.name = "SecretMissingError";
7417
+ }
7418
+ };
7419
+ new TextEncoder();
7420
+ new TextDecoder();
7421
+ var ur$2 = 3600, pr$2 = Number.MAX_SAFE_INTEGER, er$2 = Symbol("otplib.guardrails.override");
7422
+ function y$1(r, e, t) {
7423
+ if (typeof e != "number" || !Number.isSafeInteger(e)) throw new a$1(`Guardrail '${r}' must be a safe integer`);
7424
+ if (e < t) throw new a$1(`Guardrail '${r}' must be >= ${t}`);
7425
+ }
7426
+ var d$4 = Object.freeze({
6937
7427
  MIN_SECRET_BYTES: 16,
6938
7428
  MAX_SECRET_BYTES: 64,
6939
7429
  MIN_PERIOD: 1,
@@ -8577,1039 +9067,735 @@ var K512 = /* @__PURE__ */ (() => split([
8577
9067
  "0xc24b8b70d0f89791",
8578
9068
  "0xc76c51a30654be30",
8579
9069
  "0xd192e819d6ef5218",
8580
- "0xd69906245565a910",
8581
- "0xf40e35855771202a",
8582
- "0x106aa07032bbd1b8",
8583
- "0x19a4c116b8d2d0c8",
8584
- "0x1e376c085141ab53",
8585
- "0x2748774cdf8eeb99",
8586
- "0x34b0bcb5e19b48a8",
8587
- "0x391c0cb3c5c95a63",
8588
- "0x4ed8aa4ae3418acb",
8589
- "0x5b9cca4f7763e373",
8590
- "0x682e6ff3d6b2b8a3",
8591
- "0x748f82ee5defb2fc",
8592
- "0x78a5636f43172f60",
8593
- "0x84c87814a1f0ab72",
8594
- "0x8cc702081a6439ec",
8595
- "0x90befffa23631e28",
8596
- "0xa4506cebde82bde9",
8597
- "0xbef9a3f7b2c67915",
8598
- "0xc67178f2e372532b",
8599
- "0xca273eceea26619c",
8600
- "0xd186b8c721c0c207",
8601
- "0xeada7dd6cde0eb1e",
8602
- "0xf57d4f7fee6ed178",
8603
- "0x06f067aa72176fba",
8604
- "0x0a637dc5a2c898a6",
8605
- "0x113f9804bef90dae",
8606
- "0x1b710b35131c471b",
8607
- "0x28db77f523047d84",
8608
- "0x32caab7b40c72493",
8609
- "0x3c9ebe0a15c9bebc",
8610
- "0x431d67c49c100d4c",
8611
- "0x4cc5d4becb3e42b6",
8612
- "0x597f299cfc657e2a",
8613
- "0x5fcb6fab3ad6faec",
8614
- "0x6c44198c4a475817"
8615
- ].map((n) => BigInt(n))))();
8616
- var SHA512_Kh = /* @__PURE__ */ (() => K512[0])();
8617
- var SHA512_Kl = /* @__PURE__ */ (() => K512[1])();
8618
- var SHA512_W_H = /* @__PURE__ */ new Uint32Array(80);
8619
- var SHA512_W_L = /* @__PURE__ */ new Uint32Array(80);
8620
- /** Internal SHA-384 / SHA-512 compression engine from RFC 6234 §6.4. */
8621
- var SHA2_64B = class extends HashMD {
8622
- constructor(outputLen) {
8623
- super(128, outputLen, 16, false);
8624
- }
8625
- get() {
8626
- const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
8627
- return [
8628
- Ah,
8629
- Al,
8630
- Bh,
8631
- Bl,
8632
- Ch,
8633
- Cl,
8634
- Dh,
8635
- Dl,
8636
- Eh,
8637
- El,
8638
- Fh,
8639
- Fl,
8640
- Gh,
8641
- Gl,
8642
- Hh,
8643
- Hl
8644
- ];
8645
- }
8646
- set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl) {
8647
- this.Ah = Ah | 0;
8648
- this.Al = Al | 0;
8649
- this.Bh = Bh | 0;
8650
- this.Bl = Bl | 0;
8651
- this.Ch = Ch | 0;
8652
- this.Cl = Cl | 0;
8653
- this.Dh = Dh | 0;
8654
- this.Dl = Dl | 0;
8655
- this.Eh = Eh | 0;
8656
- this.El = El | 0;
8657
- this.Fh = Fh | 0;
8658
- this.Fl = Fl | 0;
8659
- this.Gh = Gh | 0;
8660
- this.Gl = Gl | 0;
8661
- this.Hh = Hh | 0;
8662
- this.Hl = Hl | 0;
8663
- }
8664
- process(view, offset) {
8665
- for (let i = 0; i < 16; i++, offset += 4) {
8666
- SHA512_W_H[i] = view.getUint32(offset);
8667
- SHA512_W_L[i] = view.getUint32(offset += 4);
8668
- }
8669
- for (let i = 16; i < 80; i++) {
8670
- const W15h = SHA512_W_H[i - 15] | 0;
8671
- const W15l = SHA512_W_L[i - 15] | 0;
8672
- const s0h = rotrSH(W15h, W15l, 1) ^ rotrSH(W15h, W15l, 8) ^ shrSH(W15h, W15l, 7);
8673
- const s0l = rotrSL(W15h, W15l, 1) ^ rotrSL(W15h, W15l, 8) ^ shrSL(W15h, W15l, 7);
8674
- const W2h = SHA512_W_H[i - 2] | 0;
8675
- const W2l = SHA512_W_L[i - 2] | 0;
8676
- const s1h = rotrSH(W2h, W2l, 19) ^ rotrBH(W2h, W2l, 61) ^ shrSH(W2h, W2l, 6);
8677
- const SUMl = add4L(s0l, rotrSL(W2h, W2l, 19) ^ rotrBL(W2h, W2l, 61) ^ shrSL(W2h, W2l, 6), SHA512_W_L[i - 7], SHA512_W_L[i - 16]);
8678
- SHA512_W_H[i] = add4H(SUMl, s0h, s1h, SHA512_W_H[i - 7], SHA512_W_H[i - 16]) | 0;
8679
- SHA512_W_L[i] = SUMl | 0;
8680
- }
8681
- let { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
8682
- for (let i = 0; i < 80; i++) {
8683
- const sigma1h = rotrSH(Eh, El, 14) ^ rotrSH(Eh, El, 18) ^ rotrBH(Eh, El, 41);
8684
- const sigma1l = rotrSL(Eh, El, 14) ^ rotrSL(Eh, El, 18) ^ rotrBL(Eh, El, 41);
8685
- const CHIh = Eh & Fh ^ ~Eh & Gh;
8686
- const CHIl = El & Fl ^ ~El & Gl;
8687
- const T1ll = add5L(Hl, sigma1l, CHIl, SHA512_Kl[i], SHA512_W_L[i]);
8688
- const T1h = add5H(T1ll, Hh, sigma1h, CHIh, SHA512_Kh[i], SHA512_W_H[i]);
8689
- const T1l = T1ll | 0;
8690
- const sigma0h = rotrSH(Ah, Al, 28) ^ rotrBH(Ah, Al, 34) ^ rotrBH(Ah, Al, 39);
8691
- const sigma0l = rotrSL(Ah, Al, 28) ^ rotrBL(Ah, Al, 34) ^ rotrBL(Ah, Al, 39);
8692
- const MAJh = Ah & Bh ^ Ah & Ch ^ Bh & Ch;
8693
- const MAJl = Al & Bl ^ Al & Cl ^ Bl & Cl;
8694
- Hh = Gh | 0;
8695
- Hl = Gl | 0;
8696
- Gh = Fh | 0;
8697
- Gl = Fl | 0;
8698
- Fh = Eh | 0;
8699
- Fl = El | 0;
8700
- ({h: Eh, l: El} = add(Dh | 0, Dl | 0, T1h | 0, T1l | 0));
8701
- Dh = Ch | 0;
8702
- Dl = Cl | 0;
8703
- Ch = Bh | 0;
8704
- Cl = Bl | 0;
8705
- Bh = Ah | 0;
8706
- Bl = Al | 0;
8707
- const All = add3L(T1l, sigma0l, MAJl);
8708
- Ah = add3H(All, T1h, sigma0h, MAJh);
8709
- Al = All | 0;
8710
- }
8711
- ({h: Ah, l: Al} = add(this.Ah | 0, this.Al | 0, Ah | 0, Al | 0));
8712
- ({h: Bh, l: Bl} = add(this.Bh | 0, this.Bl | 0, Bh | 0, Bl | 0));
8713
- ({h: Ch, l: Cl} = add(this.Ch | 0, this.Cl | 0, Ch | 0, Cl | 0));
8714
- ({h: Dh, l: Dl} = add(this.Dh | 0, this.Dl | 0, Dh | 0, Dl | 0));
8715
- ({h: Eh, l: El} = add(this.Eh | 0, this.El | 0, Eh | 0, El | 0));
8716
- ({h: Fh, l: Fl} = add(this.Fh | 0, this.Fl | 0, Fh | 0, Fl | 0));
8717
- ({h: Gh, l: Gl} = add(this.Gh | 0, this.Gl | 0, Gh | 0, Gl | 0));
8718
- ({h: Hh, l: Hl} = add(this.Hh | 0, this.Hl | 0, Hh | 0, Hl | 0));
8719
- this.set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl);
8720
- }
8721
- roundClean() {
8722
- clean(SHA512_W_H, SHA512_W_L);
8723
- }
8724
- destroy() {
8725
- this.destroyed = true;
8726
- clean(this.buffer);
8727
- this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
8728
- }
8729
- };
8730
- /** Internal SHA-512 hash class grounded in RFC 6234 §6.3 and §6.4. */
8731
- var _SHA512 = class extends SHA2_64B {
8732
- Ah = SHA512_IV[0] | 0;
8733
- Al = SHA512_IV[1] | 0;
8734
- Bh = SHA512_IV[2] | 0;
8735
- Bl = SHA512_IV[3] | 0;
8736
- Ch = SHA512_IV[4] | 0;
8737
- Cl = SHA512_IV[5] | 0;
8738
- Dh = SHA512_IV[6] | 0;
8739
- Dl = SHA512_IV[7] | 0;
8740
- Eh = SHA512_IV[8] | 0;
8741
- El = SHA512_IV[9] | 0;
8742
- Fh = SHA512_IV[10] | 0;
8743
- Fl = SHA512_IV[11] | 0;
8744
- Gh = SHA512_IV[12] | 0;
8745
- Gl = SHA512_IV[13] | 0;
8746
- Hh = SHA512_IV[14] | 0;
8747
- Hl = SHA512_IV[15] | 0;
8748
- constructor() {
8749
- super(64);
8750
- }
8751
- };
8752
- /**
8753
- * SHA2-256 hash function from RFC 4634. In JS it's the fastest: even faster than Blake3. Some info:
8754
- *
8755
- * - Trying 2^128 hashes would get 50% chance of collision, using birthday attack.
8756
- * - BTC network is doing 2^70 hashes/sec (2^95 hashes/year) as per 2025.
8757
- * - Each sha256 hash is executing 2^18 bit operations.
8758
- * - Good 2024 ASICs can do 200Th/sec with 3500 watts of power, corresponding to 2^36 hashes/joule.
8759
- * @param msg - message bytes to hash
8760
- * @returns Digest bytes.
8761
- * @example
8762
- * Hash a message with SHA2-256.
8763
- * ```ts
8764
- * sha256(new Uint8Array([97, 98, 99]));
8765
- * ```
8766
- */
8767
- var sha256 = /* @__PURE__ */ createHasher(() => new _SHA256(), /* @__PURE__ */ oidNist(1));
8768
- /**
8769
- * SHA2-512 hash function from RFC 4634.
8770
- * @param msg - message bytes to hash
8771
- * @returns Digest bytes.
8772
- * @example
8773
- * Hash a message with SHA2-512.
8774
- * ```ts
8775
- * sha512(new Uint8Array([97, 98, 99]));
8776
- * ```
8777
- */
8778
- var sha512 = /* @__PURE__ */ createHasher(() => new _SHA512(), /* @__PURE__ */ oidNist(3)), yr = new TextEncoder();
8779
- new TextDecoder();
8780
- //#endregion
8781
- //#region ../../node_modules/@otplib/plugin-crypto-noble/node_modules/@otplib/core/dist/index.js
8782
- var ur = 3600, pr = Number.MAX_SAFE_INTEGER;
8783
- Object.freeze({
8784
- MIN_SECRET_BYTES: 16,
8785
- MAX_SECRET_BYTES: 64,
8786
- MIN_PERIOD: 1,
8787
- MAX_PERIOD: ur,
8788
- MAX_COUNTER: pr,
8789
- MAX_WINDOW: 99,
8790
- [Symbol("otplib.guardrails.override")]: !1
8791
- });
8792
- function gr(r, e) {
8793
- return r.length === e.length;
8794
- }
8795
- function tr(r, e) {
8796
- let t = rr(r), o = rr(e);
8797
- if (!gr(t, o)) return !1;
8798
- let n = 0;
8799
- for (let s = 0; s < t.length; s++) n |= t[s] ^ o[s];
8800
- return n === 0;
8801
- }
8802
- function rr(r) {
8803
- return typeof r == "string" ? yr.encode(r) : r;
8804
- }
8805
- //#endregion
8806
- //#region ../../node_modules/@otplib/plugin-crypto-noble/dist/index.js
8807
- var t = class {
8808
- name = "noble";
8809
- hmac(r, n, a) {
8810
- return hmac(r === "sha1" ? sha1 : r === "sha256" ? sha256 : sha512, n, a);
8811
- }
8812
- randomBytes(r) {
8813
- return randomBytes(r);
8814
- }
8815
- constantTimeEqual(r, n) {
8816
- return tr(r, n);
8817
- }
8818
- }, A = Object.freeze(new t());
8819
- //#endregion
8820
- //#region node_modules/otplib/dist/index.js
8821
- function c(t) {
8822
- return {
8823
- secret: t.secret,
8824
- strategy: t.strategy ?? "totp",
8825
- crypto: t.crypto ?? A,
8826
- base32: t.base32 ?? d$2,
8827
- algorithm: t.algorithm ?? "sha1",
8828
- digits: t.digits ?? 6,
8829
- period: t.period ?? 30,
8830
- epoch: t.epoch ?? Math.floor(Date.now() / 1e3),
8831
- t0: t.t0 ?? 0,
8832
- counter: t.counter,
8833
- guardrails: t.guardrails ?? hr$2(),
8834
- hooks: t.hooks
8835
- };
8836
- }
8837
- function O(t) {
8838
- return {
8839
- ...c(t),
8840
- token: t.token,
8841
- epochTolerance: t.epochTolerance ?? 0,
8842
- counterTolerance: t.counterTolerance ?? 0,
8843
- afterTimeStep: t.afterTimeStep
8844
- };
8845
- }
8846
- function l(t, e, r) {
8847
- if (t === "totp") return r.totp();
8848
- if (t === "hotp") {
8849
- if (e === void 0) throw new a$2("Counter is required for HOTP strategy. Example: { strategy: 'hotp', counter: 0 }");
8850
- return r.hotp(e);
8851
- }
8852
- throw new a$2(`Unknown OTP strategy: ${t}. Valid strategies are 'totp' or 'hotp'.`);
8853
- }
8854
- function I(t) {
8855
- let { crypto: e = A, base32: r = d$2, length: a = 20 } = t || {};
8856
- return Dr({
8857
- crypto: e,
8858
- base32: r,
8859
- length: a
8860
- });
8861
- }
8862
- function T(t) {
8863
- let { strategy: e = "totp", issuer: r, label: a, secret: o, algorithm: i = "sha1", digits: s = 6, period: n = 30, counter: p } = t;
8864
- return l(e, p, {
8865
- totp: () => x$1({
8866
- issuer: r,
8867
- label: a,
8868
- secret: o,
8869
- algorithm: i,
8870
- digits: s,
8871
- period: n
8872
- }),
8873
- hotp: (y) => U$1({
8874
- issuer: r,
8875
- label: a,
8876
- secret: o,
8877
- algorithm: i,
8878
- digits: s,
8879
- counter: y
8880
- })
8881
- });
8882
- }
8883
- function d(t) {
8884
- let e = O(t), { secret: r, token: a, crypto: o, base32: i, algorithm: s, digits: n, hooks: p } = e, y = {
8885
- secret: r,
8886
- token: a,
8887
- crypto: o,
8888
- base32: i,
8889
- algorithm: s,
8890
- digits: n,
8891
- hooks: p
8892
- };
8893
- return l(e.strategy, e.counter, {
8894
- totp: () => ie({
8895
- ...y,
8896
- period: e.period,
8897
- epoch: e.epoch,
8898
- t0: e.t0,
8899
- epochTolerance: e.epochTolerance,
8900
- afterTimeStep: e.afterTimeStep,
8901
- guardrails: e.guardrails
8902
- }),
8903
- hotp: (f) => Y$1({
8904
- ...y,
8905
- counter: f,
8906
- counterTolerance: e.counterTolerance,
8907
- guardrails: e.guardrails
8908
- })
8909
- });
8910
- }
8911
- //#endregion
8912
- //#region src/auth/totp-manager.ts
8913
- /**
8914
- * TOTP / 2FA enrollment manager.
8915
- *
8916
- * Three-state lifecycle keyed by `userId` (PK of `user_totp` collection):
8917
- *
8918
- * - No row → not set up. `setup()` inserts a row with a
8919
- * fresh secret and `confirmedAt = null`.
8920
- * - `confirmedAt = null` → setup in progress; user has the secret but
8921
- * hasn't proved possession of the
8922
- * authenticator yet. `verify()` returns
8923
- * false even with a valid code — the row is
8924
- * inert until confirmed.
8925
- * - `confirmedAt != null` → enrolled; `verify()` accepts codes.
8926
- *
8927
- * Disable removes the row entirely. The next `setup()` starts fresh —
8928
- * we do NOT reuse the old secret on re-enroll (avoids the case where a
8929
- * leaked secret stays valid after a "rotation").
8930
- *
8931
- * `otplib` v13's functional API (`generateSync`/`verifySync`) uses RFC 6238
8932
- * TOTP with a default 30-second period. We pass a per-call `epochTolerance`
8933
- * of one step in either direction (±30 s) — operator clock skew is the usual
8934
- * culprit for "valid code rejected" complaints.
8935
- */
8936
- var TOTP_COLLECTION = "user_totp";
8937
- var ISSUER = "CamStack";
8938
- /** RFC 6238 default time step. otplib v13 measures `epochTolerance` in
8939
- * seconds, so a `window` of N steps maps to `N * TOTP_PERIOD_SECONDS`. */
8940
- var TOTP_PERIOD_SECONDS = 30;
8941
- function parseRow(data) {
8942
- const userId = data["userId"];
8943
- const secret = data["secret"];
8944
- const confirmedAt = data["confirmedAt"];
8945
- const createdAt = data["createdAt"];
8946
- if (typeof userId !== "string") throw new Error("user_totp row: missing userId");
8947
- if (typeof secret !== "string") throw new Error("user_totp row: missing secret");
8948
- if (typeof createdAt !== "number") throw new Error("user_totp row: missing createdAt");
8949
- return {
8950
- userId,
8951
- secret,
8952
- confirmedAt: typeof confirmedAt === "number" ? confirmedAt : null,
8953
- createdAt
8954
- };
8955
- }
8956
- var TotpManager = class {
8957
- store;
8958
- /** ±N steps of clock-skew tolerance, expressed in seconds for otplib v13's
8959
- * per-call `epochTolerance`. Default window 1 = ±30 s. */
8960
- epochTolerance;
8961
- constructor(store, opts = {}) {
8962
- this.store = store;
8963
- this.epochTolerance = (opts.window ?? 1) * TOTP_PERIOD_SECONDS;
8964
- }
8965
- /**
8966
- * Begin enrollment. Always generates a fresh secret — calling `setup`
8967
- * a second time on the same user replaces any pending half-enrollment
8968
- * (the user must rescan the QR / re-enter the secret).
8969
- *
8970
- * The user identity (`label`) is what an authenticator displays as
8971
- * the account name; we use the supplied `username` for human-readable
8972
- * recognition. `ISSUER` is the CamStack brand.
8973
- */
8974
- async setup(userId, username) {
8975
- const secret = I();
8976
- const otpauthUrl = T({
8977
- secret,
8978
- label: username,
8979
- issuer: ISSUER
8980
- });
8981
- const row = {
8982
- userId,
8983
- secret,
8984
- confirmedAt: null,
8985
- createdAt: Date.now()
8986
- };
8987
- await this.store.delete.mutate({
8988
- collection: TOTP_COLLECTION,
8989
- key: userId
8990
- });
8991
- await this.store.insert.mutate({
8992
- collection: TOTP_COLLECTION,
8993
- record: {
8994
- id: userId,
8995
- data: { ...row }
8996
- }
8997
- });
8998
- return {
8999
- secret,
9000
- otpauthUrl
9001
- };
9070
+ "0xd69906245565a910",
9071
+ "0xf40e35855771202a",
9072
+ "0x106aa07032bbd1b8",
9073
+ "0x19a4c116b8d2d0c8",
9074
+ "0x1e376c085141ab53",
9075
+ "0x2748774cdf8eeb99",
9076
+ "0x34b0bcb5e19b48a8",
9077
+ "0x391c0cb3c5c95a63",
9078
+ "0x4ed8aa4ae3418acb",
9079
+ "0x5b9cca4f7763e373",
9080
+ "0x682e6ff3d6b2b8a3",
9081
+ "0x748f82ee5defb2fc",
9082
+ "0x78a5636f43172f60",
9083
+ "0x84c87814a1f0ab72",
9084
+ "0x8cc702081a6439ec",
9085
+ "0x90befffa23631e28",
9086
+ "0xa4506cebde82bde9",
9087
+ "0xbef9a3f7b2c67915",
9088
+ "0xc67178f2e372532b",
9089
+ "0xca273eceea26619c",
9090
+ "0xd186b8c721c0c207",
9091
+ "0xeada7dd6cde0eb1e",
9092
+ "0xf57d4f7fee6ed178",
9093
+ "0x06f067aa72176fba",
9094
+ "0x0a637dc5a2c898a6",
9095
+ "0x113f9804bef90dae",
9096
+ "0x1b710b35131c471b",
9097
+ "0x28db77f523047d84",
9098
+ "0x32caab7b40c72493",
9099
+ "0x3c9ebe0a15c9bebc",
9100
+ "0x431d67c49c100d4c",
9101
+ "0x4cc5d4becb3e42b6",
9102
+ "0x597f299cfc657e2a",
9103
+ "0x5fcb6fab3ad6faec",
9104
+ "0x6c44198c4a475817"
9105
+ ].map((n) => BigInt(n))))();
9106
+ var SHA512_Kh = /* @__PURE__ */ (() => K512[0])();
9107
+ var SHA512_Kl = /* @__PURE__ */ (() => K512[1])();
9108
+ var SHA512_W_H = /* @__PURE__ */ new Uint32Array(80);
9109
+ var SHA512_W_L = /* @__PURE__ */ new Uint32Array(80);
9110
+ /** Internal SHA-384 / SHA-512 compression engine from RFC 6234 §6.4. */
9111
+ var SHA2_64B = class extends HashMD {
9112
+ constructor(outputLen) {
9113
+ super(128, outputLen, 16, false);
9002
9114
  }
9003
- /**
9004
- * Confirm enrollment by checking a code from the authenticator
9005
- * against the pending secret. On success, flips `confirmedAt` so
9006
- * `verify()` starts accepting codes. Idempotent on already-confirmed
9007
- * rows: re-confirming a valid code just succeeds with no state
9008
- * change.
9009
- */
9010
- async confirm(userId, code) {
9011
- const row = await this.find(userId);
9012
- if (!row) return false;
9013
- if (!d({
9014
- token: code,
9015
- secret: row.secret,
9016
- epochTolerance: this.epochTolerance
9017
- }).valid) return false;
9018
- if (row.confirmedAt === null) {
9019
- const next = {
9020
- ...row,
9021
- confirmedAt: Date.now()
9022
- };
9023
- await this.store.update.mutate({
9024
- collection: TOTP_COLLECTION,
9025
- id: userId,
9026
- data: { ...next }
9027
- });
9028
- }
9029
- return true;
9115
+ get() {
9116
+ const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
9117
+ return [
9118
+ Ah,
9119
+ Al,
9120
+ Bh,
9121
+ Bl,
9122
+ Ch,
9123
+ Cl,
9124
+ Dh,
9125
+ Dl,
9126
+ Eh,
9127
+ El,
9128
+ Fh,
9129
+ Fl,
9130
+ Gh,
9131
+ Gl,
9132
+ Hh,
9133
+ Hl
9134
+ ];
9030
9135
  }
9031
- /**
9032
- * Remove enrollment. Idempotent — no error if the user had no row.
9033
- */
9034
- async disable(userId) {
9035
- await this.store.delete.mutate({
9036
- collection: TOTP_COLLECTION,
9037
- key: userId
9038
- });
9136
+ set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl) {
9137
+ this.Ah = Ah | 0;
9138
+ this.Al = Al | 0;
9139
+ this.Bh = Bh | 0;
9140
+ this.Bl = Bl | 0;
9141
+ this.Ch = Ch | 0;
9142
+ this.Cl = Cl | 0;
9143
+ this.Dh = Dh | 0;
9144
+ this.Dl = Dl | 0;
9145
+ this.Eh = Eh | 0;
9146
+ this.El = El | 0;
9147
+ this.Fh = Fh | 0;
9148
+ this.Fl = Fl | 0;
9149
+ this.Gh = Gh | 0;
9150
+ this.Gl = Gl | 0;
9151
+ this.Hh = Hh | 0;
9152
+ this.Hl = Hl | 0;
9039
9153
  }
9040
- /**
9041
- * Verify a code against the confirmed secret. Returns `false` when:
9042
- * - The user has no row (not set up).
9043
- * - The row is pending (`confirmedAt === null`).
9044
- * - The code doesn't match within the configured window.
9045
- */
9046
- async verify(userId, code) {
9047
- const row = await this.find(userId);
9048
- if (!row) return false;
9049
- if (row.confirmedAt === null) return false;
9050
- return d({
9051
- token: code,
9052
- secret: row.secret,
9053
- epochTolerance: this.epochTolerance
9054
- }).valid;
9154
+ process(view, offset) {
9155
+ for (let i = 0; i < 16; i++, offset += 4) {
9156
+ SHA512_W_H[i] = view.getUint32(offset);
9157
+ SHA512_W_L[i] = view.getUint32(offset += 4);
9158
+ }
9159
+ for (let i = 16; i < 80; i++) {
9160
+ const W15h = SHA512_W_H[i - 15] | 0;
9161
+ const W15l = SHA512_W_L[i - 15] | 0;
9162
+ const s0h = rotrSH(W15h, W15l, 1) ^ rotrSH(W15h, W15l, 8) ^ shrSH(W15h, W15l, 7);
9163
+ const s0l = rotrSL(W15h, W15l, 1) ^ rotrSL(W15h, W15l, 8) ^ shrSL(W15h, W15l, 7);
9164
+ const W2h = SHA512_W_H[i - 2] | 0;
9165
+ const W2l = SHA512_W_L[i - 2] | 0;
9166
+ const s1h = rotrSH(W2h, W2l, 19) ^ rotrBH(W2h, W2l, 61) ^ shrSH(W2h, W2l, 6);
9167
+ const SUMl = add4L(s0l, rotrSL(W2h, W2l, 19) ^ rotrBL(W2h, W2l, 61) ^ shrSL(W2h, W2l, 6), SHA512_W_L[i - 7], SHA512_W_L[i - 16]);
9168
+ SHA512_W_H[i] = add4H(SUMl, s0h, s1h, SHA512_W_H[i - 7], SHA512_W_H[i - 16]) | 0;
9169
+ SHA512_W_L[i] = SUMl | 0;
9170
+ }
9171
+ let { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
9172
+ for (let i = 0; i < 80; i++) {
9173
+ const sigma1h = rotrSH(Eh, El, 14) ^ rotrSH(Eh, El, 18) ^ rotrBH(Eh, El, 41);
9174
+ const sigma1l = rotrSL(Eh, El, 14) ^ rotrSL(Eh, El, 18) ^ rotrBL(Eh, El, 41);
9175
+ const CHIh = Eh & Fh ^ ~Eh & Gh;
9176
+ const CHIl = El & Fl ^ ~El & Gl;
9177
+ const T1ll = add5L(Hl, sigma1l, CHIl, SHA512_Kl[i], SHA512_W_L[i]);
9178
+ const T1h = add5H(T1ll, Hh, sigma1h, CHIh, SHA512_Kh[i], SHA512_W_H[i]);
9179
+ const T1l = T1ll | 0;
9180
+ const sigma0h = rotrSH(Ah, Al, 28) ^ rotrBH(Ah, Al, 34) ^ rotrBH(Ah, Al, 39);
9181
+ const sigma0l = rotrSL(Ah, Al, 28) ^ rotrBL(Ah, Al, 34) ^ rotrBL(Ah, Al, 39);
9182
+ const MAJh = Ah & Bh ^ Ah & Ch ^ Bh & Ch;
9183
+ const MAJl = Al & Bl ^ Al & Cl ^ Bl & Cl;
9184
+ Hh = Gh | 0;
9185
+ Hl = Gl | 0;
9186
+ Gh = Fh | 0;
9187
+ Gl = Fl | 0;
9188
+ Fh = Eh | 0;
9189
+ Fl = El | 0;
9190
+ ({h: Eh, l: El} = add(Dh | 0, Dl | 0, T1h | 0, T1l | 0));
9191
+ Dh = Ch | 0;
9192
+ Dl = Cl | 0;
9193
+ Ch = Bh | 0;
9194
+ Cl = Bl | 0;
9195
+ Bh = Ah | 0;
9196
+ Bl = Al | 0;
9197
+ const All = add3L(T1l, sigma0l, MAJl);
9198
+ Ah = add3H(All, T1h, sigma0h, MAJh);
9199
+ Al = All | 0;
9200
+ }
9201
+ ({h: Ah, l: Al} = add(this.Ah | 0, this.Al | 0, Ah | 0, Al | 0));
9202
+ ({h: Bh, l: Bl} = add(this.Bh | 0, this.Bl | 0, Bh | 0, Bl | 0));
9203
+ ({h: Ch, l: Cl} = add(this.Ch | 0, this.Cl | 0, Ch | 0, Cl | 0));
9204
+ ({h: Dh, l: Dl} = add(this.Dh | 0, this.Dl | 0, Dh | 0, Dl | 0));
9205
+ ({h: Eh, l: El} = add(this.Eh | 0, this.El | 0, Eh | 0, El | 0));
9206
+ ({h: Fh, l: Fl} = add(this.Fh | 0, this.Fl | 0, Fh | 0, Fl | 0));
9207
+ ({h: Gh, l: Gl} = add(this.Gh | 0, this.Gl | 0, Gh | 0, Gl | 0));
9208
+ ({h: Hh, l: Hl} = add(this.Hh | 0, this.Hl | 0, Hh | 0, Hl | 0));
9209
+ this.set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl);
9055
9210
  }
9056
- /**
9057
- * Read the user's enrollment status. Pending half-enrollments are
9058
- * reported as `enabled: false` — only a confirmed row counts.
9059
- */
9060
- async getStatus(userId) {
9061
- const row = await this.find(userId);
9062
- if (!row) return {
9063
- enabled: false,
9064
- confirmedAt: null
9065
- };
9066
- return {
9067
- enabled: row.confirmedAt !== null,
9068
- confirmedAt: row.confirmedAt
9069
- };
9211
+ roundClean() {
9212
+ clean(SHA512_W_H, SHA512_W_L);
9070
9213
  }
9071
- async find(userId) {
9072
- const results = await this.store.query.query({
9073
- collection: TOTP_COLLECTION,
9074
- filter: { where: { userId } }
9075
- });
9076
- if (results.length === 0) return null;
9077
- return parseRow(results[0].data);
9214
+ destroy() {
9215
+ this.destroyed = true;
9216
+ clean(this.buffer);
9217
+ this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
9218
+ }
9219
+ };
9220
+ /** Internal SHA-512 hash class grounded in RFC 6234 §6.3 and §6.4. */
9221
+ var _SHA512 = class extends SHA2_64B {
9222
+ Ah = SHA512_IV[0] | 0;
9223
+ Al = SHA512_IV[1] | 0;
9224
+ Bh = SHA512_IV[2] | 0;
9225
+ Bl = SHA512_IV[3] | 0;
9226
+ Ch = SHA512_IV[4] | 0;
9227
+ Cl = SHA512_IV[5] | 0;
9228
+ Dh = SHA512_IV[6] | 0;
9229
+ Dl = SHA512_IV[7] | 0;
9230
+ Eh = SHA512_IV[8] | 0;
9231
+ El = SHA512_IV[9] | 0;
9232
+ Fh = SHA512_IV[10] | 0;
9233
+ Fl = SHA512_IV[11] | 0;
9234
+ Gh = SHA512_IV[12] | 0;
9235
+ Gl = SHA512_IV[13] | 0;
9236
+ Hh = SHA512_IV[14] | 0;
9237
+ Hl = SHA512_IV[15] | 0;
9238
+ constructor() {
9239
+ super(64);
9078
9240
  }
9079
9241
  };
9080
- //#endregion
9081
- //#region src/kernel/config-schema.ts
9082
- /** Default data directory — used when CAMSTACK_DATA env var is not set */
9083
- var DEFAULT_DATA_PATH = "camstack-data";
9084
- /**
9085
- * A list of browser origins from config.yaml (a YAML list) or from the
9086
- * `CAMSTACK_CORS_ORIGINS` env var (one comma-separated string — env carries
9087
- * only strings). Validation of each entry is the CORS policy's job
9088
- * (`server/backend/src/api/cors/cors-policy.ts`), which logs what it rejects;
9089
- * the schema only fixes the SHAPE so a typo cannot stop the hub from booting.
9090
- */
9091
- var originListSchema = zod.z.preprocess((value) => typeof value === "string" ? value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0) : value, zod.z.array(zod.z.string()));
9092
- var serverSchema = zod.z.object({
9093
- port: zod.z.number().default(4443),
9094
- host: zod.z.string().default("0.0.0.0"),
9095
- dataPath: zod.z.string().default(DEFAULT_DATA_PATH),
9096
- /**
9097
- * EXTRA browser origins allowed to call this hub cross-origin (D707), on
9098
- * top of the built-ins: the hub's own origin, the hosted viewer PWA and the
9099
- * native app's opaque `null` origin. Bootstrap-owned on purpose — widening
9100
- * who may read hub responses in a browser is a security decision, and the
9101
- * policy is live from the first request, before the settings store exists.
9102
- */
9103
- corsOrigins: originListSchema.default([])
9104
- });
9105
- var authSchema = zod.z.object({
9106
- jwtSecret: zod.z.string().nullable().default(null),
9107
- adminUsername: zod.z.string().default("admin"),
9108
- adminPassword: zod.z.string().default("changeme")
9109
- });
9110
- var hubSchema = zod.z.object({
9111
- url: zod.z.string().default("ws://localhost:4443/agent"),
9112
- token: zod.z.string().default("")
9113
- });
9114
- var agentSchema = zod.z.object({
9115
- name: zod.z.string().default(""),
9116
- /** Port for the agent status page (minimal HTML) */
9117
- statusPort: zod.z.number().default(4444)
9118
- });
9119
- var tlsSchema = zod.z.object({
9120
9242
  /**
9121
- * Enable HTTPS (default: true). The certificate itself is never configured
9122
- * here: the admin UI (Network → Local access) generates or uploads it
9123
- * (D726), and boot serves whatever `tls/mode` says.
9243
+ * SHA2-256 hash function from RFC 4634. In JS it's the fastest: even faster than Blake3. Some info:
9244
+ *
9245
+ * - Trying 2^128 hashes would get 50% chance of collision, using birthday attack.
9246
+ * - BTC network is doing 2^70 hashes/sec (2^95 hashes/year) as per 2025.
9247
+ * - Each sha256 hash is executing 2^18 bit operations.
9248
+ * - Good 2024 ASICs can do 200Th/sec with 3500 watts of power, corresponding to 2^36 hashes/joule.
9249
+ * @param msg - message bytes to hash
9250
+ * @returns Digest bytes.
9251
+ * @example
9252
+ * Hash a message with SHA2-256.
9253
+ * ```ts
9254
+ * sha256(new Uint8Array([97, 98, 99]));
9255
+ * ```
9124
9256
  */
9125
- enabled: zod.z.boolean().default(true) });
9257
+ var sha256 = /* @__PURE__ */ createHasher(() => new _SHA256(), /* @__PURE__ */ oidNist(1));
9126
9258
  /**
9127
- * Bootstrap-time addon configuration — read PURELY from YAML, before the
9128
- * kernel can talk to the settings-store. This breaks the chicken-and-egg
9129
- * where the settings store itself is implemented by an addon: the kernel
9130
- * needs to know WHICH addon to load first to provide the store, and that
9131
- * decision can't itself live in the store.
9132
- *
9133
- * Backward-compatible: every field is optional + defaults reproduce the
9134
- * current hard-coded behavior. Existing config.yaml files keep working.
9259
+ * SHA2-512 hash function from RFC 4634.
9260
+ * @param msg - message bytes to hash
9261
+ * @returns Digest bytes.
9262
+ * @example
9263
+ * Hash a message with SHA2-512.
9264
+ * ```ts
9265
+ * sha512(new Uint8Array([97, 98, 99]));
9266
+ * ```
9135
9267
  */
9136
- var bootstrapAddonsSchema = zod.z.object({
9137
- /**
9138
- * Addon implementing the `settings-store` capability. Loaded FIRST,
9139
- * before any other addon is enabled — every other addon's enable
9140
- * state, settings, and ordering lives in the store it provides.
9141
- *
9142
- * Default: '@camstack/addon-sqlite-storage' (the workspace's
9143
- * default SQLite-backed store). Future alternatives could swap this
9144
- * for postgres/redis without touching kernel code.
9145
- */
9146
- settingsStoreAddon: zod.z.string().default("@camstack/addon-sqlite-storage"),
9147
- /**
9148
- * How the launcher provisions REQUIRED_PACKAGES into `data/addons/`
9149
- * on startup.
9150
- *
9151
- * - `'npm'` — pull from the npm registry (production default).
9152
- * - `'local'` — copy `packages/<X>/` from the workspace into
9153
- * `data/addons/<X>/`. Use for the Electron-
9154
- * packaged bundle and the e2e harness.
9155
- * - `'symlink'` — point `data/addons/<X>` at the workspace source
9156
- * dir via a directory symlink. Dev-only: a
9157
- * workspace rebuild (`npm run build -w <X>`) is
9158
- * immediately reflected at the next addon load,
9159
- * no copy step. Falls back to `'local'` on
9160
- * platforms that reject symlink creation (Windows
9161
- * without developer-mode).
9162
- *
9163
- * The `CAMSTACK_INSTALL_SOURCE` env var, when set, wins over this
9164
- * field. Leave undefined to keep the launcher's default behaviour
9165
- * (npm when no workspace, local when a bundled addons dir is
9166
- * present, …).
9167
- */
9168
- installSource: zod.z.enum([
9169
- "npm",
9170
- "local",
9171
- "symlink"
9172
- ]).optional(),
9173
- /**
9174
- * Override the kernel's hard-coded `AddonInstaller.REQUIRED_PACKAGES`.
9175
- * Passing an array here REPLACES the default list entirely — useful
9176
- * for stripped-down deployments (only the bundle of addons the
9177
- * operator actually wants) and for forcing a different storage
9178
- * backend at first boot.
9179
- *
9180
- * Leave undefined to use the kernel-shipped defaults.
9181
- */
9182
- requiredAddons: zod.z.array(zod.z.string()).optional(),
9183
- /**
9184
- * Disable list applied AFTER the settings-store reports the enabled
9185
- * set. Operator escape hatch — boot without a known-broken addon
9186
- * without having to mutate the store.
9187
- */
9188
- forceDisable: zod.z.array(zod.z.string()).default([])
9189
- });
9190
- /** Bootstrap config -- loaded from config.yaml ONLY at startup.
9191
- * All other settings live in SQL (system_settings table). */
9192
- var bootstrapSchema = zod.z.object({
9193
- /** Server mode: 'hub' (full server) or 'agent' (worker node) */
9194
- mode: zod.z.enum(["hub", "agent"]).default("hub"),
9195
- server: serverSchema.default(serverSchema.parse({})),
9196
- auth: authSchema.default(authSchema.parse({})),
9197
- /** Hub connection config — only used when mode='agent' */
9198
- hub: hubSchema.default(hubSchema.parse({})),
9199
- /** Agent-specific config — only used when mode='agent' */
9200
- agent: agentSchema.default(agentSchema.parse({})),
9201
- /** TLS configuration */
9202
- tls: tlsSchema.default(tlsSchema.parse({})),
9203
- /**
9204
- * Addon bootstrap directives — minimum addon set needed before the
9205
- * kernel can reach the settings-store. See bootstrapAddonsSchema.
9206
- * Spec: docs/superpowers/specs/2026-05-15-yaml-bootstrap-expansion.md
9207
- */
9208
- bootstrap: bootstrapAddonsSchema.default(bootstrapAddonsSchema.parse({}))
9268
+ var sha512 = /* @__PURE__ */ createHasher(() => new _SHA512(), /* @__PURE__ */ oidNist(3)), yr = new TextEncoder();
9269
+ new TextDecoder();
9270
+ //#endregion
9271
+ //#region ../../node_modules/@otplib/plugin-crypto-noble/node_modules/@otplib/core/dist/index.js
9272
+ var ur = 3600, pr = Number.MAX_SAFE_INTEGER;
9273
+ Object.freeze({
9274
+ MIN_SECRET_BYTES: 16,
9275
+ MAX_SECRET_BYTES: 64,
9276
+ MIN_PERIOD: 1,
9277
+ MAX_PERIOD: ur,
9278
+ MAX_COUNTER: pr,
9279
+ MAX_WINDOW: 99,
9280
+ [Symbol("otplib.guardrails.override")]: !1
9209
9281
  });
9282
+ function gr(r, e) {
9283
+ return r.length === e.length;
9284
+ }
9285
+ function tr(r, e) {
9286
+ let t = rr(r), o = rr(e);
9287
+ if (!gr(t, o)) return !1;
9288
+ let n = 0;
9289
+ for (let s = 0; s < t.length; s++) n |= t[s] ^ o[s];
9290
+ return n === 0;
9291
+ }
9292
+ function rr(r) {
9293
+ return typeof r == "string" ? yr.encode(r) : r;
9294
+ }
9295
+ //#endregion
9296
+ //#region ../../node_modules/@otplib/plugin-crypto-noble/dist/index.js
9297
+ var t = class {
9298
+ name = "noble";
9299
+ hmac(r, n, a) {
9300
+ return hmac(r === "sha1" ? sha1 : r === "sha256" ? sha256 : sha512, n, a);
9301
+ }
9302
+ randomBytes(r) {
9303
+ return randomBytes(r);
9304
+ }
9305
+ constantTimeEqual(r, n) {
9306
+ return tr(r, n);
9307
+ }
9308
+ }, A = Object.freeze(new t());
9309
+ //#endregion
9310
+ //#region node_modules/otplib/dist/index.js
9311
+ function c(t) {
9312
+ return {
9313
+ secret: t.secret,
9314
+ strategy: t.strategy ?? "totp",
9315
+ crypto: t.crypto ?? A,
9316
+ base32: t.base32 ?? d$2,
9317
+ algorithm: t.algorithm ?? "sha1",
9318
+ digits: t.digits ?? 6,
9319
+ period: t.period ?? 30,
9320
+ epoch: t.epoch ?? Math.floor(Date.now() / 1e3),
9321
+ t0: t.t0 ?? 0,
9322
+ counter: t.counter,
9323
+ guardrails: t.guardrails ?? hr$2(),
9324
+ hooks: t.hooks
9325
+ };
9326
+ }
9327
+ function O(t) {
9328
+ return {
9329
+ ...c(t),
9330
+ token: t.token,
9331
+ epochTolerance: t.epochTolerance ?? 0,
9332
+ counterTolerance: t.counterTolerance ?? 0,
9333
+ afterTimeStep: t.afterTimeStep
9334
+ };
9335
+ }
9336
+ function l(t, e, r) {
9337
+ if (t === "totp") return r.totp();
9338
+ if (t === "hotp") {
9339
+ if (e === void 0) throw new a$2("Counter is required for HOTP strategy. Example: { strategy: 'hotp', counter: 0 }");
9340
+ return r.hotp(e);
9341
+ }
9342
+ throw new a$2(`Unknown OTP strategy: ${t}. Valid strategies are 'totp' or 'hotp'.`);
9343
+ }
9344
+ function I(t) {
9345
+ let { crypto: e = A, base32: r = d$2, length: a = 20 } = t || {};
9346
+ return Dr({
9347
+ crypto: e,
9348
+ base32: r,
9349
+ length: a
9350
+ });
9351
+ }
9352
+ function T(t) {
9353
+ let { strategy: e = "totp", issuer: r, label: a, secret: o, algorithm: i = "sha1", digits: s = 6, period: n = 30, counter: p } = t;
9354
+ return l(e, p, {
9355
+ totp: () => x$1({
9356
+ issuer: r,
9357
+ label: a,
9358
+ secret: o,
9359
+ algorithm: i,
9360
+ digits: s,
9361
+ period: n
9362
+ }),
9363
+ hotp: (y) => U$1({
9364
+ issuer: r,
9365
+ label: a,
9366
+ secret: o,
9367
+ algorithm: i,
9368
+ digits: s,
9369
+ counter: y
9370
+ })
9371
+ });
9372
+ }
9373
+ function d(t) {
9374
+ let e = O(t), { secret: r, token: a, crypto: o, base32: i, algorithm: s, digits: n, hooks: p } = e, y = {
9375
+ secret: r,
9376
+ token: a,
9377
+ crypto: o,
9378
+ base32: i,
9379
+ algorithm: s,
9380
+ digits: n,
9381
+ hooks: p
9382
+ };
9383
+ return l(e.strategy, e.counter, {
9384
+ totp: () => ie({
9385
+ ...y,
9386
+ period: e.period,
9387
+ epoch: e.epoch,
9388
+ t0: e.t0,
9389
+ epochTolerance: e.epochTolerance,
9390
+ afterTimeStep: e.afterTimeStep,
9391
+ guardrails: e.guardrails
9392
+ }),
9393
+ hotp: (f) => Y$1({
9394
+ ...y,
9395
+ counter: f,
9396
+ counterTolerance: e.counterTolerance,
9397
+ guardrails: e.guardrails
9398
+ })
9399
+ });
9400
+ }
9210
9401
  //#endregion
9211
- //#region src/builtins/local-auth/admin-account-boot.ts
9402
+ //#region src/auth/totp-manager.ts
9212
9403
  /**
9213
- * The admin account at boot: seed it once, reset it only on the operator's
9214
- * explicit one-shot request, and say so when it still has the default password.
9404
+ * TOTP / 2FA enrollment manager.
9215
9405
  *
9216
- * ## Why a trigger FILE, not a variable
9406
+ * Three-state lifecycle keyed by `userId` (PK of `user_totp` collection):
9217
9407
  *
9218
- * The configured password (`CAMSTACK_ADMIN_PASS` / `auth.adminPassword`) seeds
9219
- * the admin on first boot and is never applied again — a password the operator
9220
- * changes in the admin UI survives every restart. Recovering a forgotten
9221
- * password therefore needs an explicit request, and that request must be
9222
- * ONE-SHOT: an environment flag stays set in a container template until someone
9223
- * remembers to remove it, and while it is set it reverts the password on every
9224
- * boot — the very defect this replaced. A file the hub DELETES after acting on
9225
- * it cannot be left armed: create `RESET_ADMIN_PASSWORD` in the node's data
9226
- * directory (`/data` in the container), restart, and the configured admin's
9227
- * password is set back to the configured one, once.
9408
+ * - No row → not set up. `setup()` inserts a row with a
9409
+ * fresh secret and `confirmedAt = null`.
9410
+ * - `confirmedAt = null` → setup in progress; user has the secret but
9411
+ * hasn't proved possession of the
9412
+ * authenticator yet. `verify()` returns
9413
+ * false even with a valid code — the row is
9414
+ * inert until confirmed.
9415
+ * - `confirmedAt != null` → enrolled; `verify()` accepts codes.
9416
+ *
9417
+ * Disable removes the row entirely. The next `setup()` starts fresh —
9418
+ * we do NOT reuse the old secret on re-enroll (avoids the case where a
9419
+ * leaked secret stays valid after a "rotation").
9420
+ *
9421
+ * `otplib` v13's functional API (`generateSync`/`verifySync`) uses RFC 6238
9422
+ * TOTP with a default 30-second period. We pass a per-call `epochTolerance`
9423
+ * of one step in either direction (±30 s) — operator clock skew is the usual
9424
+ * culprit for "valid code rejected" complaints.
9228
9425
  */
9229
- /** The trigger's name, at the root of the node's data directory. */
9230
- var ADMIN_PASSWORD_RESET_TRIGGER = "RESET_ADMIN_PASSWORD";
9231
- async function reconcileAdminAccountAtBoot(input) {
9232
- const { users, fs, logger } = input;
9233
- const triggerPath = node_path.join(input.dataDir, ADMIN_PASSWORD_RESET_TRIGGER);
9234
- const resetRequested = fs.exists(triggerPath);
9235
- const outcome = await users.ensureAdminExists({ resetPassword: resetRequested });
9236
- logOutcome(outcome, logger);
9237
- if (resetRequested) consumeTrigger(triggerPath, outcome, input);
9238
- const username = "username" in outcome ? outcome.username : null;
9239
- const usesDefaultPassword = username !== null && await users.hasPassword(username, "changeme");
9240
- if (usesDefaultPassword && username !== null) logger.warn("The admin account still uses the default password — change it in the admin UI (the first sign-in with it asks for a new one)", { meta: { username } });
9426
+ var TOTP_COLLECTION = "user_totp";
9427
+ var ISSUER = "CamStack";
9428
+ /** RFC 6238 default time step. otplib v13 measures `epochTolerance` in
9429
+ * seconds, so a `window` of N steps maps to `N * TOTP_PERIOD_SECONDS`. */
9430
+ var TOTP_PERIOD_SECONDS = 30;
9431
+ function parseRow(data) {
9432
+ const userId = data["userId"];
9433
+ const secret = data["secret"];
9434
+ const confirmedAt = data["confirmedAt"];
9435
+ const createdAt = data["createdAt"];
9436
+ if (typeof userId !== "string") throw new Error("user_totp row: missing userId");
9437
+ if (typeof secret !== "string") throw new Error("user_totp row: missing secret");
9438
+ if (typeof createdAt !== "number") throw new Error("user_totp row: missing createdAt");
9241
9439
  return {
9242
- outcome,
9243
- usesDefaultPassword
9440
+ userId,
9441
+ secret,
9442
+ confirmedAt: typeof confirmedAt === "number" ? confirmedAt : null,
9443
+ createdAt
9244
9444
  };
9245
9445
  }
9246
- function logOutcome(outcome, logger) {
9247
- switch (outcome.kind) {
9248
- case "seeded":
9249
- logger.info("Admin account seeded from the configured credentials (first boot)", { meta: { username: outcome.username } });
9250
- return;
9251
- case "reset":
9252
- logger.warn("Admin password reset to the configured password, as requested", { meta: { username: outcome.username } });
9253
- return;
9254
- case "other-admin-exists":
9255
- logger.info("Configured admin username not seeded: another admin account already exists", { meta: { username: outcome.username } });
9256
- return;
9257
- case "not-configured":
9258
- logger.warn("No admin username/password configured — no admin account seeded");
9259
- return;
9260
- case "exists": return;
9261
- }
9262
- }
9263
- /** Delete the trigger after acting on it — a reset must never repeat by itself. */
9264
- function consumeTrigger(triggerPath, outcome, input) {
9265
- if (outcome.kind !== "reset") input.logger.warn("Admin password reset requested, but no configured admin account to reset", { meta: {
9266
- trigger: triggerPath,
9267
- outcome: outcome.kind
9268
- } });
9269
- try {
9270
- input.fs.remove(triggerPath);
9271
- } catch (err) {
9272
- input.logger.error("Could not delete the admin password reset trigger — delete it by hand, or every restart resets the admin password again", { meta: {
9273
- trigger: triggerPath,
9274
- error: err instanceof Error ? err.message : String(err)
9275
- } });
9446
+ var TotpManager = class {
9447
+ store;
9448
+ /** ±N steps of clock-skew tolerance, expressed in seconds for otplib v13's
9449
+ * per-call `epochTolerance`. Default window 1 = ±30 s. */
9450
+ epochTolerance;
9451
+ constructor(store, opts = {}) {
9452
+ this.store = store;
9453
+ this.epochTolerance = (opts.window ?? 1) * TOTP_PERIOD_SECONDS;
9276
9454
  }
9277
- }
9278
- //#endregion
9279
- //#region src/builtins/local-auth/auth-config-reader.ts
9280
- /**
9281
- * Every config path this reader can answer. Anything else resolves to
9282
- * `undefined` by construction — see the module docblock.
9283
- */
9284
- function createAuthConfigReader(resolved, writeAuthSection) {
9285
- return {
9286
- get(path) {
9287
- switch (path) {
9288
- case "auth.jwtSecret": return narrow(resolved.jwtSecret);
9289
- case "auth.adminUsername": return narrow(resolved.adminUsername);
9290
- case "auth.adminPassword": return narrow(resolved.adminPassword);
9291
- case "auth.tokenExpiry": return narrow(resolved.tokenExpiry);
9292
- default: return;
9455
+ /**
9456
+ * Begin enrollment. Always generates a fresh secret — calling `setup`
9457
+ * a second time on the same user replaces any pending half-enrollment
9458
+ * (the user must rescan the QR / re-enter the secret).
9459
+ *
9460
+ * The user identity (`label`) is what an authenticator displays as
9461
+ * the account name; we use the supplied `username` for human-readable
9462
+ * recognition. `ISSUER` is the CamStack brand.
9463
+ */
9464
+ async setup(userId, username) {
9465
+ const secret = I();
9466
+ const otpauthUrl = T({
9467
+ secret,
9468
+ label: username,
9469
+ issuer: ISSUER
9470
+ });
9471
+ const row = {
9472
+ userId,
9473
+ secret,
9474
+ confirmedAt: null,
9475
+ createdAt: Date.now()
9476
+ };
9477
+ await this.store.delete.mutate({
9478
+ collection: TOTP_COLLECTION,
9479
+ key: userId
9480
+ });
9481
+ await this.store.insert.mutate({
9482
+ collection: TOTP_COLLECTION,
9483
+ record: {
9484
+ id: userId,
9485
+ data: { ...row }
9293
9486
  }
9294
- },
9295
- update(_section, data) {
9296
- if (typeof data["jwtSecret"] === "string") writeAuthSection({ jwtSecret: data["jwtSecret"] });
9487
+ });
9488
+ return {
9489
+ secret,
9490
+ otpauthUrl
9491
+ };
9492
+ }
9493
+ /**
9494
+ * Confirm enrollment by checking a code from the authenticator
9495
+ * against the pending secret. On success, flips `confirmedAt` so
9496
+ * `verify()` starts accepting codes. Idempotent on already-confirmed
9497
+ * rows: re-confirming a valid code just succeeds with no state
9498
+ * change.
9499
+ */
9500
+ async confirm(userId, code) {
9501
+ const row = await this.find(userId);
9502
+ if (!row) return false;
9503
+ if (!d({
9504
+ token: code,
9505
+ secret: row.secret,
9506
+ epochTolerance: this.epochTolerance
9507
+ }).valid) return false;
9508
+ if (row.confirmedAt === null) {
9509
+ const next = {
9510
+ ...row,
9511
+ confirmedAt: Date.now()
9512
+ };
9513
+ await this.store.update.mutate({
9514
+ collection: TOTP_COLLECTION,
9515
+ id: userId,
9516
+ data: { ...next }
9517
+ });
9297
9518
  }
9298
- };
9299
- }
9300
- /**
9301
- * The one documented type boundary in this module. `AuthConfigReader.get` is
9302
- * the legacy `get<T>(path)` shape shared with `ConfigManager.get`, which is
9303
- * likewise unchecked at runtime — every value returned here is a `string |
9304
- * undefined` the caller immediately re-validates (`parseTokenExpiry`, the
9305
- * truthiness gate on `jwtSecret`).
9306
- */
9307
- function narrow(value) {
9308
- return value;
9309
- }
9310
- //#endregion
9311
- //#region src/builtins/local-auth/auth-schema.ts
9312
- /**
9313
- * @durable class=config owner=local-auth
9314
- * write="an operator creates a user, or `ensureAdminExists` seeds the configured
9315
- * admin on first boot (or resets its password on the operator's one-shot
9316
- * RESET_ADMIN_PASSWORD trigger); updated on scope, isAdmin and password edits"
9317
- * retention="none — a row goes only when an operator deletes the user. A
9318
- * `declareCollection` that adds a PK/UNIQUE/NOT-NULL-without-default column
9319
- * REBUILDS the table: every account is wiped and only the configured admin
9320
- * comes back on the next boot."
9321
- */
9322
- var USERS_COLLECTION = "users";
9323
- /**
9324
- * @durable class=config owner=local-auth
9325
- * write="an operator mints an API key; `validateToken` rewrites `lastUsedAt` on
9326
- * every request the key authorises"
9327
- * retention="none — an explicit `revoke` is the only delete. API keys have no
9328
- * expiry column and no sweep, so a key lives until someone removes it (or the
9329
- * destructive `declareCollection` rebuild takes the table)."
9330
- */
9331
- var API_KEYS_COLLECTION = "api_keys";
9519
+ return true;
9520
+ }
9521
+ /**
9522
+ * Remove enrollment. Idempotent — no error if the user had no row.
9523
+ */
9524
+ async disable(userId) {
9525
+ await this.store.delete.mutate({
9526
+ collection: TOTP_COLLECTION,
9527
+ key: userId
9528
+ });
9529
+ }
9530
+ /**
9531
+ * Verify a code against the confirmed secret. Returns `false` when:
9532
+ * - The user has no row (not set up).
9533
+ * - The row is pending (`confirmedAt === null`).
9534
+ * - The code doesn't match within the configured window.
9535
+ */
9536
+ async verify(userId, code) {
9537
+ const row = await this.find(userId);
9538
+ if (!row) return false;
9539
+ if (row.confirmedAt === null) return false;
9540
+ return d({
9541
+ token: code,
9542
+ secret: row.secret,
9543
+ epochTolerance: this.epochTolerance
9544
+ }).valid;
9545
+ }
9546
+ /**
9547
+ * Read the user's enrollment status. Pending half-enrollments are
9548
+ * reported as `enabled: false` — only a confirmed row counts.
9549
+ */
9550
+ async getStatus(userId) {
9551
+ const row = await this.find(userId);
9552
+ if (!row) return {
9553
+ enabled: false,
9554
+ confirmedAt: null
9555
+ };
9556
+ return {
9557
+ enabled: row.confirmedAt !== null,
9558
+ confirmedAt: row.confirmedAt
9559
+ };
9560
+ }
9561
+ async find(userId) {
9562
+ const results = await this.store.query.query({
9563
+ collection: TOTP_COLLECTION,
9564
+ filter: { where: { userId } }
9565
+ });
9566
+ if (results.length === 0) return null;
9567
+ return parseRow(results[0].data);
9568
+ }
9569
+ };
9570
+ //#endregion
9571
+ //#region src/kernel/config-schema.ts
9572
+ /** Default data directory — used when CAMSTACK_DATA env var is not set */
9573
+ var DEFAULT_DATA_PATH = "camstack-data";
9332
9574
  /**
9333
- * @durable class=config owner=local-auth
9334
- * write="an operator or the CLI mints a scoped token; `validate` fires
9335
- * `updateLastUsed` on every accepted call"
9336
- * retention="`revoke` deletes one. The daily sweep `reapExpired` drops rows past
9337
- * `expiresAt` — a NULL expiry is a deliberate never-expires token and BETWEEN
9338
- * never matches it. `cleanupOrphans` drops tokens owned by the legacy 'system'
9339
- * id or by a user that no longer exists."
9575
+ * A list of browser origins from config.yaml (a YAML list) or from the
9576
+ * `CAMSTACK_CORS_ORIGINS` env var (one comma-separated string — env carries
9577
+ * only strings). Validation of each entry is the CORS policy's job
9578
+ * (`server/backend/src/api/cors/cors-policy.ts`), which logs what it rejects;
9579
+ * the schema only fixes the SHAPE so a typo cannot stop the hub from booting.
9340
9580
  */
9341
- var SCOPED_TOKENS_COLLECTION = "scoped_tokens";
9581
+ var originListSchema = zod.z.preprocess((value) => typeof value === "string" ? value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0) : value, zod.z.array(zod.z.string()));
9582
+ var serverSchema = zod.z.object({
9583
+ port: zod.z.number().default(4443),
9584
+ host: zod.z.string().default("0.0.0.0"),
9585
+ dataPath: zod.z.string().default(DEFAULT_DATA_PATH),
9586
+ /**
9587
+ * EXTRA browser origins allowed to call this hub cross-origin (D707), on
9588
+ * top of the built-ins: the hub's own origin, the hosted viewer PWA and the
9589
+ * native app's opaque `null` origin. Bootstrap-owned on purpose — widening
9590
+ * who may read hub responses in a browser is a security decision, and the
9591
+ * policy is live from the first request, before the settings store exists.
9592
+ */
9593
+ corsOrigins: originListSchema.default([])
9594
+ });
9595
+ var authSchema = zod.z.object({
9596
+ jwtSecret: zod.z.string().nullable().default(null),
9597
+ adminUsername: zod.z.string().default("admin"),
9598
+ adminPassword: zod.z.string().default("changeme")
9599
+ });
9600
+ var hubSchema = zod.z.object({
9601
+ url: zod.z.string().default("ws://localhost:4443/agent"),
9602
+ token: zod.z.string().default("")
9603
+ });
9604
+ var agentSchema = zod.z.object({
9605
+ name: zod.z.string().default(""),
9606
+ /** Port for the agent status page (minimal HTML) */
9607
+ statusPort: zod.z.number().default(4444)
9608
+ });
9609
+ var tlsSchema = zod.z.object({
9342
9610
  /**
9343
- * @durable class=ledger owner=local-auth
9344
- * write="one row per redeemed OAuth code — every completed account link through
9345
- * `oauthExchangeCode`; `touch` rewrites `lastUsedAt` on each refresh"
9346
- * retention="ONLY revoked rows are pruned: the daily sweep deletes sessions
9347
- * revoked more than OAUTH_REVOKED_RETENTION_MS (30 d) ago. An ACTIVE session
9348
- * has `revokedAt` NULL, never expires and is never swept — the table grows one
9349
- * row per login until an operator revokes the link."
9611
+ * Enable HTTPS (default: true). The certificate itself is never configured
9612
+ * here: the admin UI (Network → Local access) generates or uploads it
9613
+ * (D726), and boot serves whatever `tls/mode` says.
9350
9614
  */
9351
- var OAUTH_SESSIONS_COLLECTION = "oauth_sessions";
9615
+ enabled: zod.z.boolean().default(true) });
9352
9616
  /**
9353
- * TOTP enrollment table — split off from `users` to avoid the
9354
- * destructive drop-+-recreate migration `ensureTable` performs when a
9355
- * new column is added to a typed schema (see `sqlite-settings-backend.ts`).
9356
- *
9357
- * Lifecycle:
9358
- * - Setup: row inserted with `confirmedAt = null` and a fresh secret.
9359
- * - Confirm: a valid code from the secret flips `confirmedAt` to now().
9360
- * - Disable: the row is deleted (next setup starts fresh).
9361
- *
9362
- * "Enabled" means `confirmedAt != null`. A half-enrolled row (`confirmedAt
9363
- * = null`) is INACTIVE — `verifyTotp` returns false until confirmation.
9617
+ * Bootstrap-time addon configuration — read PURELY from YAML, before the
9618
+ * kernel can talk to the settings-store. This breaks the chicken-and-egg
9619
+ * where the settings store itself is implemented by an addon: the kernel
9620
+ * needs to know WHICH addon to load first to provide the store, and that
9621
+ * decision can't itself live in the store.
9364
9622
  *
9365
- * @durable class=config owner=local-auth
9366
- * write="`setup` inserts a fresh secret for the user (deleting any pending row
9367
- * first); `confirm` flips `confirmedAt` once a valid code proves possession"
9368
- * retention="`disable` deletes the row, and `setup` deletes before re-inserting.
9369
- * Nothing else prunes — deleting a USER does not cascade here, so their
9370
- * enrollment row outlives the account."
9371
- */
9372
- var USER_TOTP_COLLECTION = "user_totp";
9373
- var USERS_COLUMNS = [
9374
- {
9375
- name: "id",
9376
- type: "TEXT",
9377
- primaryKey: true,
9378
- notNull: true
9379
- },
9380
- {
9381
- name: "username",
9382
- type: "TEXT",
9383
- notNull: true,
9384
- unique: true
9385
- },
9386
- {
9387
- name: "passwordHash",
9388
- type: "TEXT",
9389
- notNull: true
9390
- },
9391
- {
9392
- name: "isAdmin",
9393
- type: "BOOLEAN",
9394
- notNull: true
9395
- },
9396
- {
9397
- name: "allowedProviders",
9398
- type: "JSON"
9399
- },
9400
- {
9401
- name: "allowedDevices",
9402
- type: "JSON"
9403
- },
9404
- {
9405
- name: "scopes",
9406
- type: "JSON"
9407
- },
9408
- {
9409
- name: "createdAt",
9410
- type: "INTEGER",
9411
- notNull: true
9412
- },
9413
- {
9414
- name: "updatedAt",
9415
- type: "INTEGER",
9416
- notNull: true
9417
- }
9418
- ];
9419
- var API_KEYS_COLUMNS = [
9420
- {
9421
- name: "id",
9422
- type: "TEXT",
9423
- primaryKey: true,
9424
- notNull: true
9425
- },
9426
- {
9427
- name: "label",
9428
- type: "TEXT",
9429
- notNull: true
9430
- },
9431
- {
9432
- name: "isAdmin",
9433
- type: "BOOLEAN",
9434
- notNull: true
9435
- },
9436
- {
9437
- name: "tokenHash",
9438
- type: "TEXT",
9439
- notNull: true,
9440
- unique: true
9441
- },
9442
- {
9443
- name: "tokenPrefix",
9444
- type: "TEXT",
9445
- notNull: true
9446
- },
9447
- {
9448
- name: "allowedProviders",
9449
- type: "JSON"
9450
- },
9451
- {
9452
- name: "allowedDevices",
9453
- type: "JSON"
9454
- },
9455
- {
9456
- name: "createdAt",
9457
- type: "INTEGER",
9458
- notNull: true
9459
- },
9460
- {
9461
- name: "lastUsedAt",
9462
- type: "INTEGER"
9463
- }
9464
- ];
9465
- var USER_TOTP_COLUMNS = [
9466
- {
9467
- name: "userId",
9468
- type: "TEXT",
9469
- primaryKey: true,
9470
- notNull: true
9471
- },
9472
- {
9473
- name: "secret",
9474
- type: "TEXT",
9475
- notNull: true
9476
- },
9477
- {
9478
- name: "confirmedAt",
9479
- type: "INTEGER"
9480
- },
9481
- {
9482
- name: "createdAt",
9483
- type: "INTEGER",
9484
- notNull: true
9485
- }
9486
- ];
9487
- var SCOPED_TOKENS_COLUMNS = [
9488
- {
9489
- name: "id",
9490
- type: "TEXT",
9491
- primaryKey: true,
9492
- notNull: true
9493
- },
9494
- {
9495
- name: "userId",
9496
- type: "TEXT",
9497
- notNull: true
9498
- },
9499
- {
9500
- name: "name",
9501
- type: "TEXT",
9502
- notNull: true
9503
- },
9504
- {
9505
- name: "tokenHash",
9506
- type: "TEXT",
9507
- notNull: true,
9508
- unique: true
9509
- },
9510
- {
9511
- name: "tokenPrefix",
9512
- type: "TEXT",
9513
- notNull: true
9514
- },
9515
- {
9516
- name: "scopes",
9517
- type: "JSON"
9518
- },
9519
- {
9520
- name: "expiresAt",
9521
- type: "INTEGER"
9522
- },
9523
- {
9524
- name: "lastUsedAt",
9525
- type: "INTEGER"
9526
- },
9527
- {
9528
- name: "createdAt",
9529
- type: "INTEGER",
9530
- notNull: true
9623
+ * Backward-compatible: every field is optional + defaults reproduce the
9624
+ * current hard-coded behavior. Existing config.yaml files keep working.
9625
+ */
9626
+ var bootstrapAddonsSchema = zod.z.object({
9627
+ /**
9628
+ * Addon implementing the `settings-store` capability. Loaded FIRST,
9629
+ * before any other addon is enabled — every other addon's enable
9630
+ * state, settings, and ordering lives in the store it provides.
9631
+ *
9632
+ * Default: '@camstack/addon-sqlite-storage' (the workspace's
9633
+ * default SQLite-backed store). Future alternatives could swap this
9634
+ * for postgres/redis without touching kernel code.
9635
+ */
9636
+ settingsStoreAddon: zod.z.string().default("@camstack/addon-sqlite-storage"),
9637
+ /**
9638
+ * How the launcher provisions REQUIRED_PACKAGES into `data/addons/`
9639
+ * on startup.
9640
+ *
9641
+ * - `'npm'` — pull from the npm registry (production default).
9642
+ * - `'local'` — copy `packages/<X>/` from the workspace into
9643
+ * `data/addons/<X>/`. Use for the Electron-
9644
+ * packaged bundle and the e2e harness.
9645
+ * - `'symlink'` — point `data/addons/<X>` at the workspace source
9646
+ * dir via a directory symlink. Dev-only: a
9647
+ * workspace rebuild (`npm run build -w <X>`) is
9648
+ * immediately reflected at the next addon load,
9649
+ * no copy step. Falls back to `'local'` on
9650
+ * platforms that reject symlink creation (Windows
9651
+ * without developer-mode).
9652
+ *
9653
+ * The `CAMSTACK_INSTALL_SOURCE` env var, when set, wins over this
9654
+ * field. Leave undefined to keep the launcher's default behaviour
9655
+ * (npm when no workspace, local when a bundled addons dir is
9656
+ * present, …).
9657
+ */
9658
+ installSource: zod.z.enum([
9659
+ "npm",
9660
+ "local",
9661
+ "symlink"
9662
+ ]).optional(),
9663
+ /**
9664
+ * Override the kernel's hard-coded `AddonInstaller.REQUIRED_PACKAGES`.
9665
+ * Passing an array here REPLACES the default list entirely — useful
9666
+ * for stripped-down deployments (only the bundle of addons the
9667
+ * operator actually wants) and for forcing a different storage
9668
+ * backend at first boot.
9669
+ *
9670
+ * Leave undefined to use the kernel-shipped defaults.
9671
+ */
9672
+ requiredAddons: zod.z.array(zod.z.string()).optional(),
9673
+ /**
9674
+ * Disable list applied AFTER the settings-store reports the enabled
9675
+ * set. Operator escape hatch — boot without a known-broken addon
9676
+ * without having to mutate the store.
9677
+ */
9678
+ forceDisable: zod.z.array(zod.z.string()).default([])
9679
+ });
9680
+ /** Bootstrap config -- loaded from config.yaml ONLY at startup.
9681
+ * All other settings live in SQL (system_settings table). */
9682
+ var bootstrapSchema = zod.z.object({
9683
+ /** Server mode: 'hub' (full server) or 'agent' (worker node) */
9684
+ mode: zod.z.enum(["hub", "agent"]).default("hub"),
9685
+ server: serverSchema.default(serverSchema.parse({})),
9686
+ auth: authSchema.default(authSchema.parse({})),
9687
+ /** Hub connection config — only used when mode='agent' */
9688
+ hub: hubSchema.default(hubSchema.parse({})),
9689
+ /** Agent-specific config — only used when mode='agent' */
9690
+ agent: agentSchema.default(agentSchema.parse({})),
9691
+ /** TLS configuration */
9692
+ tls: tlsSchema.default(tlsSchema.parse({})),
9693
+ /**
9694
+ * Addon bootstrap directives — minimum addon set needed before the
9695
+ * kernel can reach the settings-store. See bootstrapAddonsSchema.
9696
+ * Spec: docs/superpowers/specs/2026-05-15-yaml-bootstrap-expansion.md
9697
+ */
9698
+ bootstrap: bootstrapAddonsSchema.default(bootstrapAddonsSchema.parse({}))
9699
+ });
9700
+ //#endregion
9701
+ //#region src/builtins/local-auth/admin-account-boot.ts
9702
+ /**
9703
+ * The admin account at boot: seed it once, reset it only on the operator's
9704
+ * explicit one-shot request, and say so when it still has the default password.
9705
+ *
9706
+ * ## Why a trigger FILE, not a variable
9707
+ *
9708
+ * The configured password (`CAMSTACK_ADMIN_PASS` / `auth.adminPassword`) seeds
9709
+ * the admin on first boot and is never applied again — a password the operator
9710
+ * changes in the admin UI survives every restart. Recovering a forgotten
9711
+ * password therefore needs an explicit request, and that request must be
9712
+ * ONE-SHOT: an environment flag stays set in a container template until someone
9713
+ * remembers to remove it, and while it is set it reverts the password on every
9714
+ * boot — the very defect this replaced. A file the hub DELETES after acting on
9715
+ * it cannot be left armed: create `RESET_ADMIN_PASSWORD` in the node's data
9716
+ * directory (`/data` in the container), restart, and the configured admin's
9717
+ * password is set back to the configured one, once.
9718
+ */
9719
+ /** The trigger's name, at the root of the node's data directory. */
9720
+ var ADMIN_PASSWORD_RESET_TRIGGER = "RESET_ADMIN_PASSWORD";
9721
+ async function reconcileAdminAccountAtBoot(input) {
9722
+ const { users, fs, logger } = input;
9723
+ const triggerPath = node_path.join(input.dataDir, ADMIN_PASSWORD_RESET_TRIGGER);
9724
+ const resetRequested = fs.exists(triggerPath);
9725
+ const outcome = await users.ensureAdminExists({ resetPassword: resetRequested });
9726
+ logOutcome(outcome, logger);
9727
+ if (resetRequested) consumeTrigger(triggerPath, outcome, input);
9728
+ const username = "username" in outcome ? outcome.username : null;
9729
+ const usesDefaultPassword = username !== null && await users.hasPassword(username, "changeme");
9730
+ if (usesDefaultPassword && username !== null) logger.warn("The admin account still uses the default password — change it in the admin UI (the first sign-in with it asks for a new one)", { meta: { username } });
9731
+ return {
9732
+ outcome,
9733
+ usesDefaultPassword
9734
+ };
9735
+ }
9736
+ function logOutcome(outcome, logger) {
9737
+ switch (outcome.kind) {
9738
+ case "seeded":
9739
+ logger.info("Admin account seeded from the configured credentials (first boot)", { meta: { username: outcome.username } });
9740
+ return;
9741
+ case "reset":
9742
+ logger.warn("Admin password reset to the configured password, as requested", { meta: { username: outcome.username } });
9743
+ return;
9744
+ case "other-admin-exists":
9745
+ logger.info("Configured admin username not seeded: another admin account already exists", { meta: { username: outcome.username } });
9746
+ return;
9747
+ case "not-configured":
9748
+ logger.warn("No admin username/password configured — no admin account seeded");
9749
+ return;
9750
+ case "exists": return;
9531
9751
  }
9532
- ];
9533
- var OAUTH_SESSIONS_COLUMNS = [
9534
- {
9535
- name: "id",
9536
- type: "TEXT",
9537
- primaryKey: true,
9538
- notNull: true
9539
- },
9540
- {
9541
- name: "userId",
9542
- type: "TEXT",
9543
- notNull: true
9544
- },
9545
- {
9546
- name: "username",
9547
- type: "TEXT",
9548
- notNull: true
9549
- },
9550
- {
9551
- name: "integrationId",
9552
- type: "TEXT",
9553
- notNull: true
9554
- },
9555
- {
9556
- name: "scopes",
9557
- type: "JSON"
9558
- },
9559
- {
9560
- name: "createdAt",
9561
- type: "INTEGER",
9562
- notNull: true
9563
- },
9564
- {
9565
- name: "lastUsedAt",
9566
- type: "INTEGER",
9567
- notNull: true
9568
- },
9569
- {
9570
- name: "revokedAt",
9571
- type: "INTEGER"
9752
+ }
9753
+ /** Delete the trigger after acting on it — a reset must never repeat by itself. */
9754
+ function consumeTrigger(triggerPath, outcome, input) {
9755
+ if (outcome.kind !== "reset") input.logger.warn("Admin password reset requested, but no configured admin account to reset", { meta: {
9756
+ trigger: triggerPath,
9757
+ outcome: outcome.kind
9758
+ } });
9759
+ try {
9760
+ input.fs.remove(triggerPath);
9761
+ } catch (err) {
9762
+ input.logger.error("Could not delete the admin password reset trigger — delete it by hand, or every restart resets the admin password again", { meta: {
9763
+ trigger: triggerPath,
9764
+ error: err instanceof Error ? err.message : String(err)
9765
+ } });
9572
9766
  }
9573
- ];
9767
+ }
9768
+ //#endregion
9769
+ //#region src/builtins/local-auth/auth-config-reader.ts
9574
9770
  /**
9575
- * Declare every auth collection with the typed schema. Called by
9576
- * `LocalAuthAddon.onInitialize` before constructing the per-collection
9577
- * managers. Idempotent — `declareCollection` is a no-op when the
9578
- * collection has already been declared (same shape).
9771
+ * Every config path this reader can answer. Anything else resolves to
9772
+ * `undefined` by construction — see the module docblock.
9579
9773
  */
9580
- async function declareAuthSchema(store) {
9581
- await store.declareCollection.mutate({
9582
- collection: USERS_COLLECTION,
9583
- columns: USERS_COLUMNS
9584
- });
9585
- await store.declareCollection.mutate({
9586
- collection: API_KEYS_COLLECTION,
9587
- columns: API_KEYS_COLUMNS,
9588
- indexes: [{
9589
- name: "idx_api_keys_token_prefix",
9590
- columns: ["tokenPrefix"]
9591
- }]
9592
- });
9593
- await store.declareCollection.mutate({
9594
- collection: SCOPED_TOKENS_COLLECTION,
9595
- columns: SCOPED_TOKENS_COLUMNS,
9596
- indexes: [{
9597
- name: "idx_scoped_tokens_user_id",
9598
- columns: ["userId"]
9599
- }]
9600
- });
9601
- await store.declareCollection.mutate({
9602
- collection: USER_TOTP_COLLECTION,
9603
- columns: USER_TOTP_COLUMNS
9604
- });
9605
- await store.declareCollection.mutate({
9606
- collection: OAUTH_SESSIONS_COLLECTION,
9607
- columns: OAUTH_SESSIONS_COLUMNS,
9608
- indexes: [{
9609
- name: "idx_oauth_sessions_user_id",
9610
- columns: ["userId"]
9611
- }]
9612
- });
9774
+ function createAuthConfigReader(resolved, writeAuthSection) {
9775
+ return {
9776
+ get(path) {
9777
+ switch (path) {
9778
+ case "auth.jwtSecret": return narrow(resolved.jwtSecret);
9779
+ case "auth.adminUsername": return narrow(resolved.adminUsername);
9780
+ case "auth.adminPassword": return narrow(resolved.adminPassword);
9781
+ case "auth.tokenExpiry": return narrow(resolved.tokenExpiry);
9782
+ default: return;
9783
+ }
9784
+ },
9785
+ update(_section, data) {
9786
+ if (typeof data["jwtSecret"] === "string") writeAuthSection({ jwtSecret: data["jwtSecret"] });
9787
+ }
9788
+ };
9789
+ }
9790
+ /**
9791
+ * The one documented type boundary in this module. `AuthConfigReader.get` is
9792
+ * the legacy `get<T>(path)` shape shared with `ConfigManager.get`, which is
9793
+ * likewise unchecked at runtime — every value returned here is a `string |
9794
+ * undefined` the caller immediately re-validates (`parseTokenExpiry`, the
9795
+ * truthiness gate on `jwtSecret`).
9796
+ */
9797
+ function narrow(value) {
9798
+ return value;
9613
9799
  }
9614
9800
  //#endregion
9615
9801
  //#region src/builtins/local-auth/oauth-grants.ts
@@ -9965,6 +10151,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
9965
10151
  oauthSessionManager = null;
9966
10152
  reapTimer = null;
9967
10153
  totpManager = null;
10154
+ externalIdentities = null;
9968
10155
  constructor() {
9969
10156
  super({
9970
10157
  jwtSecret: "",
@@ -9994,6 +10181,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
9994
10181
  this.scopedTokenManager = new ScopedTokenManager(store);
9995
10182
  this.oauthSessionManager = new OauthSessionManager(store);
9996
10183
  this.totpManager = new TotpManager(store);
10184
+ this.externalIdentities = new ExternalIdentityManager(store);
9997
10185
  try {
9998
10186
  await reconcileAdminAccountAtBoot({
9999
10187
  users: this.userManager,
@@ -10086,6 +10274,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
10086
10274
  deleteUser: async (input) => {
10087
10275
  if (!this.userManager) throw new Error("User management not available");
10088
10276
  await this.userManager.delete(input.id);
10277
+ await this.externalIdentities?.removeAllForUser(input.id);
10089
10278
  return { success: true };
10090
10279
  },
10091
10280
  resetPassword: async (input) => {
@@ -10179,6 +10368,28 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
10179
10368
  revokedAt: s.revokedAt
10180
10369
  }));
10181
10370
  },
10371
+ listExternalIdentities: async (input) => {
10372
+ if (!this.externalIdentities) return [];
10373
+ return [...await this.externalIdentities.listForUser(input.userId)];
10374
+ },
10375
+ findUserByExternalIdentity: async (input) => {
10376
+ if (!this.externalIdentities) return null;
10377
+ const link = await this.externalIdentities.findLink(input.provider, input.subject);
10378
+ return link === null ? null : {
10379
+ userId: link.userId,
10380
+ issuer: link.issuer
10381
+ };
10382
+ },
10383
+ linkExternalIdentity: async (input) => {
10384
+ if (!this.externalIdentities || !this.userManager) throw new Error("External sign-in linking not available");
10385
+ if (!await this.userManager.findById(input.userId)) throw new Error(`User not found: ${input.userId}`);
10386
+ await this.externalIdentities.link(input);
10387
+ return { success: true };
10388
+ },
10389
+ unlinkExternalIdentity: async (input) => {
10390
+ if (!this.externalIdentities) return { success: false };
10391
+ return { success: await this.externalIdentities.unlink(input) };
10392
+ },
10182
10393
  revokeOauthSession: async (input) => {
10183
10394
  if (!this.oauthSessionManager) return { success: false };
10184
10395
  return { success: await this.oauthSessionManager.markRevoked(input.id) };
@@ -10252,6 +10463,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
10252
10463
  this.apiKeyManager = null;
10253
10464
  this.scopedTokenManager = null;
10254
10465
  this.oauthSessionManager = null;
10466
+ this.externalIdentities = null;
10255
10467
  }
10256
10468
  };
10257
10469
  //#endregion