@camstack/system 1.2.441 → 1.2.442

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 (85) hide show
  1. package/dist/addon-runner.js +72 -2
  2. package/dist/addon-runner.mjs +72 -2
  3. package/dist/auth/totp-manager.d.ts +46 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.d.ts +41 -1
  9. package/dist/builtins/alerts/alerts.addon.js +638 -26
  10. package/dist/builtins/alerts/alerts.addon.mjs +638 -26
  11. package/dist/builtins/alerts/critical-push.d.ts +86 -0
  12. package/dist/builtins/alerts/runner-alert.d.ts +71 -0
  13. package/dist/builtins/autotrack/index.js +1 -1
  14. package/dist/builtins/autotrack/index.mjs +1 -1
  15. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  16. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  17. package/dist/builtins/calendars/calendars.addon.js +1 -1
  18. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  19. package/dist/builtins/camera-grid/index.js +1 -1
  20. package/dist/builtins/camera-grid/index.mjs +1 -1
  21. package/dist/builtins/composer/composer.addon.js +2 -2
  22. package/dist/builtins/composer/composer.addon.mjs +2 -2
  23. package/dist/builtins/console-logging/index.js +1 -1
  24. package/dist/builtins/console-logging/index.mjs +1 -1
  25. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  26. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  27. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  28. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  29. package/dist/builtins/hub-forwarder/index.js +1 -1
  30. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  31. package/dist/builtins/keychain/index.js +3 -3
  32. package/dist/builtins/keychain/index.mjs +3 -3
  33. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  34. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  35. package/dist/builtins/local-auth/auth-schema.d.ts +1 -1
  36. package/dist/builtins/local-auth/local-auth.addon.js +226 -17
  37. package/dist/builtins/local-auth/local-auth.addon.mjs +226 -17
  38. package/dist/builtins/local-network/local-network.addon.js +2 -2
  39. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  40. package/dist/builtins/loki-logging/index.js +1 -1
  41. package/dist/builtins/loki-logging/index.mjs +1 -1
  42. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  43. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  44. package/dist/builtins/platform-probe/index.js +1 -1
  45. package/dist/builtins/platform-probe/index.mjs +1 -1
  46. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  47. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  48. package/dist/builtins/snapshot/index.js +1 -1
  49. package/dist/builtins/snapshot/index.mjs +1 -1
  50. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  51. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  52. package/dist/builtins/sqlite-storage/index.js +1 -1
  53. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  54. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  55. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  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 +2 -2
  59. package/dist/builtins/system-config/system-config.addon.mjs +2 -2
  60. package/dist/builtins/winston-logging/index.js +1 -1
  61. package/dist/builtins/winston-logging/index.mjs +1 -1
  62. package/dist/{composition-sources-ByNISOnW.mjs → composition-sources-CftPpY4O.mjs} +1 -1
  63. package/dist/{composition-sources-BG-W2U0d.js → composition-sources-DmSEMtUD.js} +1 -1
  64. package/dist/{constants-DJILnUSb.mjs → constants-2iWQ-Nx-.mjs} +1 -1
  65. package/dist/{constants-UojhAJcm.js → constants-b7kpjuO4.js} +1 -1
  66. package/dist/{device-ca-D8Ri9TVz.mjs → device-ca-BJpYTAPg.mjs} +1 -1
  67. package/dist/{device-ca-196DAEZT.js → device-ca-tKY_ZS9H.js} +1 -1
  68. package/dist/{dist-BiTCcaf-.js → dist-CyIHDLCg.js} +158 -8
  69. package/dist/{dist-BWvkuc31.mjs → dist-DsrQYcLm.mjs} +153 -9
  70. package/dist/file-host-worker.js +1 -1
  71. package/dist/file-host-worker.mjs +1 -1
  72. package/dist/{hub-mdns-ciao-Cnd0bTB9.mjs → hub-mdns-ciao-BS2akjWx.mjs} +1 -1
  73. package/dist/{hub-mdns-ciao-DOBTq8qr.js → hub-mdns-ciao-Da-0NEyr.js} +1 -1
  74. package/dist/index.js +142 -13
  75. package/dist/index.mjs +137 -14
  76. package/dist/kernel/addon-refused-error.d.ts +30 -0
  77. package/dist/kernel/index.d.ts +3 -0
  78. package/dist/kernel/legacy-blob-guard.d.ts +6 -1
  79. package/dist/kernel/moleculer/process-service.d.ts +16 -1
  80. package/dist/kernel/moleculer/runner-refusal.d.ts +27 -0
  81. package/dist/{restore-archive-CfSSjKnA.js → restore-archive-Cdf09dHm.js} +1 -1
  82. package/dist/{restore-archive-4ziluTU3.mjs → restore-archive-RaA3uZ7H.mjs} +1 -1
  83. package/dist/{wal-maintenance-CgZTNWAz.js → wal-maintenance-C-bmkDhm.js} +0 -0
  84. package/dist/{wal-maintenance-B57SAp4a.mjs → wal-maintenance-DE8QrreK.mjs} +0 -0
  85. package/package.json +1 -1
@@ -3,10 +3,10 @@ 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-BiTCcaf-.js");
6
+ const require_dist = require("../../dist-CyIHDLCg.js");
7
7
  const require_safe_buffer$1 = require("../../safe-buffer-D3aoa-7X.js");
8
8
  const require_ms$1 = require("../../ms-LB3giGoU.js");
9
- const require_device_ca = require("../../device-ca-196DAEZT.js");
9
+ const require_device_ca = require("../../device-ca-tKY_ZS9H.js");
10
10
  let node_crypto = require("node:crypto");
11
11
  node_crypto = require_chunk.__toESM(node_crypto);
12
12
  let crypto$1 = require("crypto");
@@ -8293,7 +8293,7 @@ var OAUTH_SESSIONS_COLLECTION = "oauth_sessions";
8293
8293
  * = null`) is INACTIVE — `verifyTotp` returns false until confirmation.
8294
8294
  *
8295
8295
  * @durable class=config owner=local-auth
8296
- * write="`setup` inserts a fresh secret for the user (deleting any pending row
8296
+ * write="`setup` inserts a keychain marker for the user's seed (D878; deleting any pending row
8297
8297
  * first); `confirm` flips `confirmedAt` once a valid code proves possession"
8298
8298
  * retention="`disable` deletes the row, and `setup` deletes before re-inserting.
8299
8299
  * Nothing else prunes — deleting a USER does not cascade here, so their
@@ -10912,6 +10912,16 @@ function d(t) {
10912
10912
  * we do NOT reuse the old secret on re-enroll (avoids the case where a
10913
10913
  * leaked secret stays valid after a "rotation").
10914
10914
  *
10915
+ * ## Where the seed lives (D878)
10916
+ *
10917
+ * The seed is a keychain secret (owner `system`, ref
10918
+ * `secret://system/row/user_totp/<userId>/secret`); the row's `secret` column
10919
+ * holds the JSON keychain marker, never the seed. A row written before D878
10920
+ * still holds the seed inline and keeps working until the operator's
10921
+ * `migrateRawSecrets` moves it (nothing migrates at boot). A seed the keychain
10922
+ * cannot serve makes `confirm`/`verify` THROW {@link TotpSecretUnavailableError}:
10923
+ * 2FA fails closed, never accepts a code and never reads as "not enrolled".
10924
+ *
10915
10925
  * `otplib` v13's functional API (`generateSync`/`verifySync`) uses RFC 6238
10916
10926
  * TOTP with a default 30-second period. We pass a per-call `epochTolerance`
10917
10927
  * of one step in either direction (±30 s) — operator clock skew is the usual
@@ -10937,13 +10947,52 @@ function parseRow(data) {
10937
10947
  createdAt
10938
10948
  };
10939
10949
  }
10950
+ /**
10951
+ * The seed cannot be used right now. The message is what a not-yet-logged-in
10952
+ * caller sees, so it is generic; `reason` (keychain detail) is for the server log only.
10953
+ */
10954
+ var TotpSecretUnavailableError = class extends Error {
10955
+ reason;
10956
+ constructor(reason) {
10957
+ super("two-factor verification is temporarily unavailable; try again later or ask an administrator");
10958
+ this.reason = reason;
10959
+ this.name = "TotpSecretUnavailableError";
10960
+ }
10961
+ };
10962
+ /** A seed cannot be stored: setup is refused, never written as plaintext. */
10963
+ var TotpSecretStorageUnavailableError = class extends Error {
10964
+ constructor(reason) {
10965
+ super(`two-factor setup is unavailable: the TOTP seed cannot be stored in the keychain (${reason}).`);
10966
+ this.name = "TotpSecretStorageUnavailableError";
10967
+ }
10968
+ };
10969
+ /** What the stored column is: a keychain marker, a pre-D878 inline seed, or garbage (never an inline seed). */
10970
+ function classify(column) {
10971
+ if (column.length === 0) return { kind: "invalid" };
10972
+ if (!column.startsWith("{")) return { kind: "inline" };
10973
+ try {
10974
+ const parsed = JSON.parse(column);
10975
+ return require_dist.isSecretMarker(parsed) ? {
10976
+ kind: "marker",
10977
+ ref: parsed.$secret
10978
+ } : { kind: "invalid" };
10979
+ } catch {
10980
+ return { kind: "invalid" };
10981
+ }
10982
+ }
10940
10983
  var TotpManager = class {
10941
10984
  store;
10942
10985
  /** ±N steps of clock-skew tolerance, expressed in seconds for otplib v13's
10943
10986
  * per-call `epochTolerance`. Default window 1 = ±30 s. */
10944
10987
  epochTolerance;
10988
+ secrets;
10989
+ logger;
10990
+ /** Per-user queue: setup / confirm / disable / migration never interleave on one user. */
10991
+ chains = /* @__PURE__ */ new Map();
10945
10992
  constructor(store, opts = {}) {
10946
10993
  this.store = store;
10994
+ this.secrets = opts.secrets ?? null;
10995
+ this.logger = opts.logger ?? null;
10947
10996
  this.epochTolerance = (opts.window ?? 1) * TOTP_PERIOD_SECONDS;
10948
10997
  }
10949
10998
  /**
@@ -10955,23 +11004,35 @@ var TotpManager = class {
10955
11004
  * the account name; we use the supplied `username` for human-readable
10956
11005
  * recognition. `ISSUER` is the CamStack brand.
10957
11006
  */
10958
- async setup(userId, username) {
11007
+ setup(userId, username) {
11008
+ return this.serialize(userId, () => this.setupLocked(userId, username));
11009
+ }
11010
+ async setupLocked(userId, username) {
10959
11011
  const secret = I();
10960
11012
  const otpauthUrl = T({
10961
11013
  secret,
10962
11014
  label: username,
10963
11015
  issuer: ISSUER
10964
11016
  });
11017
+ const ref = this.refFor(userId);
11018
+ if (this.secrets === null || ref === null) throw new TotpSecretStorageUnavailableError("this process has no keychain door");
11019
+ const state = await this.secrets.state().catch(() => null);
11020
+ if (state === null || state.state !== "ready") throw new TotpSecretStorageUnavailableError("the keychain is locked");
11021
+ await this.store.delete.mutate({
11022
+ collection: TOTP_COLLECTION,
11023
+ key: userId
11024
+ });
11025
+ try {
11026
+ await this.secrets.put(ref, secret);
11027
+ } catch (err) {
11028
+ throw new TotpSecretStorageUnavailableError(err instanceof Error ? err.message : String(err));
11029
+ }
10965
11030
  const row = {
10966
11031
  userId,
10967
- secret,
11032
+ secret: JSON.stringify(require_dist.makeSecretMarker(ref)),
10968
11033
  confirmedAt: null,
10969
11034
  createdAt: Date.now()
10970
11035
  };
10971
- await this.store.delete.mutate({
10972
- collection: TOTP_COLLECTION,
10973
- key: userId
10974
- });
10975
11036
  await this.store.insert.mutate({
10976
11037
  collection: TOTP_COLLECTION,
10977
11038
  record: {
@@ -10991,12 +11052,15 @@ var TotpManager = class {
10991
11052
  * rows: re-confirming a valid code just succeeds with no state
10992
11053
  * change.
10993
11054
  */
10994
- async confirm(userId, code) {
11055
+ confirm(userId, code) {
11056
+ return this.serialize(userId, () => this.confirmLocked(userId, code));
11057
+ }
11058
+ async confirmLocked(userId, code) {
10995
11059
  const row = await this.find(userId);
10996
11060
  if (!row) return false;
10997
11061
  if (!d({
10998
11062
  token: code,
10999
- secret: row.secret,
11063
+ secret: await this.seedOf(row),
11000
11064
  epochTolerance: this.epochTolerance
11001
11065
  }).valid) return false;
11002
11066
  if (row.confirmedAt === null) {
@@ -11015,11 +11079,23 @@ var TotpManager = class {
11015
11079
  /**
11016
11080
  * Remove enrollment. Idempotent — no error if the user had no row.
11017
11081
  */
11018
- async disable(userId) {
11019
- await this.store.delete.mutate({
11020
- collection: TOTP_COLLECTION,
11021
- key: userId
11082
+ disable(userId) {
11083
+ return this.serialize(userId, async () => {
11084
+ await this.store.delete.mutate({
11085
+ collection: TOTP_COLLECTION,
11086
+ key: userId
11087
+ });
11088
+ await this.dropSeed(userId);
11089
+ });
11090
+ }
11091
+ serialize(userId, fn) {
11092
+ const run = (this.chains.get(userId) ?? Promise.resolve()).then(fn, fn);
11093
+ const tail = run.catch(() => void 0);
11094
+ this.chains.set(userId, tail);
11095
+ tail.then(() => {
11096
+ if (this.chains.get(userId) === tail) this.chains.delete(userId);
11022
11097
  });
11098
+ return run;
11023
11099
  }
11024
11100
  /**
11025
11101
  * Verify a code against the confirmed secret. Returns `false` when:
@@ -11033,7 +11109,7 @@ var TotpManager = class {
11033
11109
  if (row.confirmedAt === null) return false;
11034
11110
  return d({
11035
11111
  token: code,
11036
- secret: row.secret,
11112
+ secret: await this.seedOf(row),
11037
11113
  epochTolerance: this.epochTolerance
11038
11114
  }).valid;
11039
11115
  }
@@ -11052,6 +11128,132 @@ var TotpManager = class {
11052
11128
  confirmedAt: row.confirmedAt
11053
11129
  };
11054
11130
  }
11131
+ refFor(userId) {
11132
+ return this.secrets === null ? null : this.secrets.ref({
11133
+ kind: "row",
11134
+ collection: TOTP_COLLECTION,
11135
+ rowId: userId
11136
+ }, "secret");
11137
+ }
11138
+ /** A refused read: generic to the caller, the keychain detail to the server log. */
11139
+ unavailable(userId, reason) {
11140
+ this.logger?.warn("TOTP seed unavailable", {
11141
+ tags: { userId },
11142
+ meta: { reason }
11143
+ });
11144
+ return new TotpSecretUnavailableError(reason);
11145
+ }
11146
+ /** The seed in clear: from the keychain for a marker row, inline for a pre-D878 row. Throws when unreadable. */
11147
+ async seedOf(row) {
11148
+ const column = classify(row.secret);
11149
+ if (column.kind === "inline") return row.secret;
11150
+ if (column.kind === "invalid") throw this.unavailable(row.userId, "malformed seed column");
11151
+ if (this.secrets === null || column.ref !== this.refFor(row.userId)) throw this.unavailable(row.userId, "marker does not belong to this user");
11152
+ let answer;
11153
+ try {
11154
+ answer = (await this.secrets.resolve([column.ref]))[column.ref];
11155
+ } catch (err) {
11156
+ throw this.unavailable(row.userId, err instanceof Error ? err.message : String(err));
11157
+ }
11158
+ if (answer?.kind === "value") return answer.value;
11159
+ throw this.unavailable(row.userId, answer?.kind === "unavailable" ? answer.reason : answer?.kind ?? "no-answer");
11160
+ }
11161
+ /** Remove a user's seed from the keychain; a miss leaves an orphan the Secrets page shows. */
11162
+ async dropSeed(userId) {
11163
+ const ref = this.refFor(userId);
11164
+ if (ref === null) return;
11165
+ try {
11166
+ await this.secrets?.delete([ref]);
11167
+ } catch {}
11168
+ }
11169
+ /**
11170
+ * D878: move every inline seed into the keychain. Dry run unless `apply`.
11171
+ * Per row: put -> read back and compare -> only then rewrite the row with the
11172
+ * marker, so the inline seed is cleared by the verified move and nowhere
11173
+ * else. Counts only; idempotent.
11174
+ */
11175
+ async migrateRawSecrets(apply) {
11176
+ const rows = await this.readAll();
11177
+ let raw = 0;
11178
+ let markers = 0;
11179
+ let migrated = 0;
11180
+ let failed = 0;
11181
+ for (const row of rows) {
11182
+ const kind = classify(row.secret).kind;
11183
+ if (kind === "marker") {
11184
+ markers += 1;
11185
+ continue;
11186
+ }
11187
+ if (kind !== "inline") continue;
11188
+ raw += 1;
11189
+ if (!apply) continue;
11190
+ if (await this.moveRow(row)) migrated += 1;
11191
+ else failed += 1;
11192
+ }
11193
+ let remainingRaw = 0;
11194
+ let unresolvedMarkers = 0;
11195
+ for (const row of await this.readAll()) {
11196
+ if (classify(row.secret).kind === "inline") {
11197
+ remainingRaw += 1;
11198
+ continue;
11199
+ }
11200
+ try {
11201
+ await this.seedOf(row);
11202
+ } catch {
11203
+ unresolvedMarkers += 1;
11204
+ }
11205
+ }
11206
+ return {
11207
+ apply,
11208
+ rows: rows.length,
11209
+ raw,
11210
+ markers,
11211
+ migrated,
11212
+ failed,
11213
+ remainingRaw,
11214
+ unresolvedMarkers,
11215
+ verified: remainingRaw === 0 && unresolvedMarkers === 0
11216
+ };
11217
+ }
11218
+ /**
11219
+ * Compare-and-set under the user's queue: the row is re-read and moved only if
11220
+ * it still holds the very inline seed we planned to move, so a concurrent
11221
+ * `setup` (its seed and marker) is never overwritten. The ref is always the
11222
+ * user's own, never taken from the row.
11223
+ */
11224
+ moveRow(planned) {
11225
+ return this.serialize(planned.userId, async () => {
11226
+ const ref = this.refFor(planned.userId);
11227
+ if (this.secrets === null || ref === null) return false;
11228
+ try {
11229
+ const current = await this.find(planned.userId);
11230
+ if (current === null || current.secret !== planned.secret) return false;
11231
+ await this.secrets.put(ref, planned.secret);
11232
+ const back = (await this.secrets.resolve([ref]))[ref];
11233
+ if (back?.kind !== "value" || back.value !== planned.secret) return false;
11234
+ const next = {
11235
+ ...current,
11236
+ secret: JSON.stringify(require_dist.makeSecretMarker(ref))
11237
+ };
11238
+ await this.store.update.mutate({
11239
+ collection: TOTP_COLLECTION,
11240
+ id: planned.userId,
11241
+ data: { ...next }
11242
+ });
11243
+ return true;
11244
+ } catch {
11245
+ return false;
11246
+ }
11247
+ });
11248
+ }
11249
+ async readAll() {
11250
+ const results = await this.store.query.query({ collection: TOTP_COLLECTION });
11251
+ const out = [];
11252
+ for (const r of results) try {
11253
+ out.push(parseRow(r.data));
11254
+ } catch {}
11255
+ return out;
11256
+ }
11055
11257
  async find(userId) {
11056
11258
  const results = await this.store.query.query({
11057
11259
  collection: TOTP_COLLECTION,
@@ -11549,7 +11751,10 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
11549
11751
  this.apiKeyManager = new ApiKeyManager(storageAccess);
11550
11752
  this.scopedTokenManager = new ScopedTokenManager(store);
11551
11753
  this.oauthSessionManager = new OauthSessionManager(store);
11552
- this.totpManager = new TotpManager(store);
11754
+ this.totpManager = new TotpManager(store, {
11755
+ secrets: forOwner === null ? null : forOwner("system"),
11756
+ logger: this.ctx.logger
11757
+ });
11553
11758
  this.externalIdentities = new ExternalIdentityManager(store);
11554
11759
  this.loginSessions = new LoginSessionManager(store, { logger: this.ctx.logger });
11555
11760
  await this.loginSessions.load();
@@ -11791,6 +11996,10 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
11791
11996
  };
11792
11997
  return this.totpManager.getStatus(input.userId);
11793
11998
  },
11999
+ migrateRawSecrets: async (input) => {
12000
+ if (!this.totpManager) throw new Error("TOTP management not available");
12001
+ return this.totpManager.migrateRawSecrets(input.apply);
12002
+ },
11794
12003
  verifyTotp: async (input) => {
11795
12004
  if (!this.totpManager) return { valid: false };
11796
12005
  return { valid: await this.totpManager.verify(input.userId, input.code) };
@@ -1,8 +1,8 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { E as DEVICE_NONCE_TTL_MS, It as deviceCredentialsCapability, Q as SsoBridgeClaimsSchema, T as DEVICE_ENROLLMENT_TTL_MS, X as ScopedTokenSchema, Zt as formatDeviceRevoker, a as ApiKeyRecordSchema, cr as userManagementCapability, ct as authProviderCapability, jr as BaseAddon, mt as buildDeviceSignatureMessage, pt as buildDeviceEnrollmentLink, rt as UserRecordSchema, xi as userError } from "../../dist-BWvkuc31.mjs";
2
+ import { E as DEVICE_NONCE_TTL_MS, It as deviceCredentialsCapability, Q as SsoBridgeClaimsSchema, Si as userError, T as DEVICE_ENROLLMENT_TTL_MS, X as ScopedTokenSchema, Zt as formatDeviceRevoker, a as ApiKeyRecordSchema, ai as isSecretMarker, cr as userManagementCapability, ct as authProviderCapability, jr as BaseAddon, mt as buildDeviceSignatureMessage, pt as buildDeviceEnrollmentLink, rt as UserRecordSchema, si as makeSecretMarker } from "../../dist-DsrQYcLm.mjs";
3
3
  import { t as require_safe_buffer } from "../../safe-buffer-B1OjL1I9.mjs";
4
4
  import { t as require_ms } from "../../ms-CgH2OFlD.mjs";
5
- import { _ as setProcessJwtSigningKey, a as importDevicePublicKey, c as rebuildDeviceCaCertificate, d as JWT_SIGNING_KEY_REF, g as processJwtSigningKey, h as loadJwtSigningKey, i as generateDeviceCa, l as verifyDeviceLeaf, m as getProcessJwtSigningKeyState, n as DEVICE_CA_KEY_REF, o as issueDeviceLeaf, r as DEVICE_CA_SETTINGS_SECTION, s as loadDeviceCa, t as DEVICE_CA_FIELD, u as verifyDeviceSignature } from "../../device-ca-D8Ri9TVz.mjs";
5
+ import { _ as setProcessJwtSigningKey, a as importDevicePublicKey, c as rebuildDeviceCaCertificate, d as JWT_SIGNING_KEY_REF, g as processJwtSigningKey, h as loadJwtSigningKey, i as generateDeviceCa, l as verifyDeviceLeaf, m as getProcessJwtSigningKeyState, n as DEVICE_CA_KEY_REF, o as issueDeviceLeaf, r as DEVICE_CA_SETTINGS_SECTION, s as loadDeviceCa, t as DEVICE_CA_FIELD, u as verifyDeviceSignature } from "../../device-ca-BJpYTAPg.mjs";
6
6
  import * as crypto$1 from "node:crypto";
7
7
  import { X509Certificate, createHash, randomUUID, timingSafeEqual } from "node:crypto";
8
8
  import nodeCrypto from "crypto";
@@ -8283,7 +8283,7 @@ var OAUTH_SESSIONS_COLLECTION = "oauth_sessions";
8283
8283
  * = null`) is INACTIVE — `verifyTotp` returns false until confirmation.
8284
8284
  *
8285
8285
  * @durable class=config owner=local-auth
8286
- * write="`setup` inserts a fresh secret for the user (deleting any pending row
8286
+ * write="`setup` inserts a keychain marker for the user's seed (D878; deleting any pending row
8287
8287
  * first); `confirm` flips `confirmedAt` once a valid code proves possession"
8288
8288
  * retention="`disable` deletes the row, and `setup` deletes before re-inserting.
8289
8289
  * Nothing else prunes — deleting a USER does not cascade here, so their
@@ -10902,6 +10902,16 @@ function d(t) {
10902
10902
  * we do NOT reuse the old secret on re-enroll (avoids the case where a
10903
10903
  * leaked secret stays valid after a "rotation").
10904
10904
  *
10905
+ * ## Where the seed lives (D878)
10906
+ *
10907
+ * The seed is a keychain secret (owner `system`, ref
10908
+ * `secret://system/row/user_totp/<userId>/secret`); the row's `secret` column
10909
+ * holds the JSON keychain marker, never the seed. A row written before D878
10910
+ * still holds the seed inline and keeps working until the operator's
10911
+ * `migrateRawSecrets` moves it (nothing migrates at boot). A seed the keychain
10912
+ * cannot serve makes `confirm`/`verify` THROW {@link TotpSecretUnavailableError}:
10913
+ * 2FA fails closed, never accepts a code and never reads as "not enrolled".
10914
+ *
10905
10915
  * `otplib` v13's functional API (`generateSync`/`verifySync`) uses RFC 6238
10906
10916
  * TOTP with a default 30-second period. We pass a per-call `epochTolerance`
10907
10917
  * of one step in either direction (±30 s) — operator clock skew is the usual
@@ -10927,13 +10937,52 @@ function parseRow(data) {
10927
10937
  createdAt
10928
10938
  };
10929
10939
  }
10940
+ /**
10941
+ * The seed cannot be used right now. The message is what a not-yet-logged-in
10942
+ * caller sees, so it is generic; `reason` (keychain detail) is for the server log only.
10943
+ */
10944
+ var TotpSecretUnavailableError = class extends Error {
10945
+ reason;
10946
+ constructor(reason) {
10947
+ super("two-factor verification is temporarily unavailable; try again later or ask an administrator");
10948
+ this.reason = reason;
10949
+ this.name = "TotpSecretUnavailableError";
10950
+ }
10951
+ };
10952
+ /** A seed cannot be stored: setup is refused, never written as plaintext. */
10953
+ var TotpSecretStorageUnavailableError = class extends Error {
10954
+ constructor(reason) {
10955
+ super(`two-factor setup is unavailable: the TOTP seed cannot be stored in the keychain (${reason}).`);
10956
+ this.name = "TotpSecretStorageUnavailableError";
10957
+ }
10958
+ };
10959
+ /** What the stored column is: a keychain marker, a pre-D878 inline seed, or garbage (never an inline seed). */
10960
+ function classify(column) {
10961
+ if (column.length === 0) return { kind: "invalid" };
10962
+ if (!column.startsWith("{")) return { kind: "inline" };
10963
+ try {
10964
+ const parsed = JSON.parse(column);
10965
+ return isSecretMarker(parsed) ? {
10966
+ kind: "marker",
10967
+ ref: parsed.$secret
10968
+ } : { kind: "invalid" };
10969
+ } catch {
10970
+ return { kind: "invalid" };
10971
+ }
10972
+ }
10930
10973
  var TotpManager = class {
10931
10974
  store;
10932
10975
  /** ±N steps of clock-skew tolerance, expressed in seconds for otplib v13's
10933
10976
  * per-call `epochTolerance`. Default window 1 = ±30 s. */
10934
10977
  epochTolerance;
10978
+ secrets;
10979
+ logger;
10980
+ /** Per-user queue: setup / confirm / disable / migration never interleave on one user. */
10981
+ chains = /* @__PURE__ */ new Map();
10935
10982
  constructor(store, opts = {}) {
10936
10983
  this.store = store;
10984
+ this.secrets = opts.secrets ?? null;
10985
+ this.logger = opts.logger ?? null;
10937
10986
  this.epochTolerance = (opts.window ?? 1) * TOTP_PERIOD_SECONDS;
10938
10987
  }
10939
10988
  /**
@@ -10945,23 +10994,35 @@ var TotpManager = class {
10945
10994
  * the account name; we use the supplied `username` for human-readable
10946
10995
  * recognition. `ISSUER` is the CamStack brand.
10947
10996
  */
10948
- async setup(userId, username) {
10997
+ setup(userId, username) {
10998
+ return this.serialize(userId, () => this.setupLocked(userId, username));
10999
+ }
11000
+ async setupLocked(userId, username) {
10949
11001
  const secret = I();
10950
11002
  const otpauthUrl = T({
10951
11003
  secret,
10952
11004
  label: username,
10953
11005
  issuer: ISSUER
10954
11006
  });
11007
+ const ref = this.refFor(userId);
11008
+ if (this.secrets === null || ref === null) throw new TotpSecretStorageUnavailableError("this process has no keychain door");
11009
+ const state = await this.secrets.state().catch(() => null);
11010
+ if (state === null || state.state !== "ready") throw new TotpSecretStorageUnavailableError("the keychain is locked");
11011
+ await this.store.delete.mutate({
11012
+ collection: TOTP_COLLECTION,
11013
+ key: userId
11014
+ });
11015
+ try {
11016
+ await this.secrets.put(ref, secret);
11017
+ } catch (err) {
11018
+ throw new TotpSecretStorageUnavailableError(err instanceof Error ? err.message : String(err));
11019
+ }
10955
11020
  const row = {
10956
11021
  userId,
10957
- secret,
11022
+ secret: JSON.stringify(makeSecretMarker(ref)),
10958
11023
  confirmedAt: null,
10959
11024
  createdAt: Date.now()
10960
11025
  };
10961
- await this.store.delete.mutate({
10962
- collection: TOTP_COLLECTION,
10963
- key: userId
10964
- });
10965
11026
  await this.store.insert.mutate({
10966
11027
  collection: TOTP_COLLECTION,
10967
11028
  record: {
@@ -10981,12 +11042,15 @@ var TotpManager = class {
10981
11042
  * rows: re-confirming a valid code just succeeds with no state
10982
11043
  * change.
10983
11044
  */
10984
- async confirm(userId, code) {
11045
+ confirm(userId, code) {
11046
+ return this.serialize(userId, () => this.confirmLocked(userId, code));
11047
+ }
11048
+ async confirmLocked(userId, code) {
10985
11049
  const row = await this.find(userId);
10986
11050
  if (!row) return false;
10987
11051
  if (!d({
10988
11052
  token: code,
10989
- secret: row.secret,
11053
+ secret: await this.seedOf(row),
10990
11054
  epochTolerance: this.epochTolerance
10991
11055
  }).valid) return false;
10992
11056
  if (row.confirmedAt === null) {
@@ -11005,11 +11069,23 @@ var TotpManager = class {
11005
11069
  /**
11006
11070
  * Remove enrollment. Idempotent — no error if the user had no row.
11007
11071
  */
11008
- async disable(userId) {
11009
- await this.store.delete.mutate({
11010
- collection: TOTP_COLLECTION,
11011
- key: userId
11072
+ disable(userId) {
11073
+ return this.serialize(userId, async () => {
11074
+ await this.store.delete.mutate({
11075
+ collection: TOTP_COLLECTION,
11076
+ key: userId
11077
+ });
11078
+ await this.dropSeed(userId);
11079
+ });
11080
+ }
11081
+ serialize(userId, fn) {
11082
+ const run = (this.chains.get(userId) ?? Promise.resolve()).then(fn, fn);
11083
+ const tail = run.catch(() => void 0);
11084
+ this.chains.set(userId, tail);
11085
+ tail.then(() => {
11086
+ if (this.chains.get(userId) === tail) this.chains.delete(userId);
11012
11087
  });
11088
+ return run;
11013
11089
  }
11014
11090
  /**
11015
11091
  * Verify a code against the confirmed secret. Returns `false` when:
@@ -11023,7 +11099,7 @@ var TotpManager = class {
11023
11099
  if (row.confirmedAt === null) return false;
11024
11100
  return d({
11025
11101
  token: code,
11026
- secret: row.secret,
11102
+ secret: await this.seedOf(row),
11027
11103
  epochTolerance: this.epochTolerance
11028
11104
  }).valid;
11029
11105
  }
@@ -11042,6 +11118,132 @@ var TotpManager = class {
11042
11118
  confirmedAt: row.confirmedAt
11043
11119
  };
11044
11120
  }
11121
+ refFor(userId) {
11122
+ return this.secrets === null ? null : this.secrets.ref({
11123
+ kind: "row",
11124
+ collection: TOTP_COLLECTION,
11125
+ rowId: userId
11126
+ }, "secret");
11127
+ }
11128
+ /** A refused read: generic to the caller, the keychain detail to the server log. */
11129
+ unavailable(userId, reason) {
11130
+ this.logger?.warn("TOTP seed unavailable", {
11131
+ tags: { userId },
11132
+ meta: { reason }
11133
+ });
11134
+ return new TotpSecretUnavailableError(reason);
11135
+ }
11136
+ /** The seed in clear: from the keychain for a marker row, inline for a pre-D878 row. Throws when unreadable. */
11137
+ async seedOf(row) {
11138
+ const column = classify(row.secret);
11139
+ if (column.kind === "inline") return row.secret;
11140
+ if (column.kind === "invalid") throw this.unavailable(row.userId, "malformed seed column");
11141
+ if (this.secrets === null || column.ref !== this.refFor(row.userId)) throw this.unavailable(row.userId, "marker does not belong to this user");
11142
+ let answer;
11143
+ try {
11144
+ answer = (await this.secrets.resolve([column.ref]))[column.ref];
11145
+ } catch (err) {
11146
+ throw this.unavailable(row.userId, err instanceof Error ? err.message : String(err));
11147
+ }
11148
+ if (answer?.kind === "value") return answer.value;
11149
+ throw this.unavailable(row.userId, answer?.kind === "unavailable" ? answer.reason : answer?.kind ?? "no-answer");
11150
+ }
11151
+ /** Remove a user's seed from the keychain; a miss leaves an orphan the Secrets page shows. */
11152
+ async dropSeed(userId) {
11153
+ const ref = this.refFor(userId);
11154
+ if (ref === null) return;
11155
+ try {
11156
+ await this.secrets?.delete([ref]);
11157
+ } catch {}
11158
+ }
11159
+ /**
11160
+ * D878: move every inline seed into the keychain. Dry run unless `apply`.
11161
+ * Per row: put -> read back and compare -> only then rewrite the row with the
11162
+ * marker, so the inline seed is cleared by the verified move and nowhere
11163
+ * else. Counts only; idempotent.
11164
+ */
11165
+ async migrateRawSecrets(apply) {
11166
+ const rows = await this.readAll();
11167
+ let raw = 0;
11168
+ let markers = 0;
11169
+ let migrated = 0;
11170
+ let failed = 0;
11171
+ for (const row of rows) {
11172
+ const kind = classify(row.secret).kind;
11173
+ if (kind === "marker") {
11174
+ markers += 1;
11175
+ continue;
11176
+ }
11177
+ if (kind !== "inline") continue;
11178
+ raw += 1;
11179
+ if (!apply) continue;
11180
+ if (await this.moveRow(row)) migrated += 1;
11181
+ else failed += 1;
11182
+ }
11183
+ let remainingRaw = 0;
11184
+ let unresolvedMarkers = 0;
11185
+ for (const row of await this.readAll()) {
11186
+ if (classify(row.secret).kind === "inline") {
11187
+ remainingRaw += 1;
11188
+ continue;
11189
+ }
11190
+ try {
11191
+ await this.seedOf(row);
11192
+ } catch {
11193
+ unresolvedMarkers += 1;
11194
+ }
11195
+ }
11196
+ return {
11197
+ apply,
11198
+ rows: rows.length,
11199
+ raw,
11200
+ markers,
11201
+ migrated,
11202
+ failed,
11203
+ remainingRaw,
11204
+ unresolvedMarkers,
11205
+ verified: remainingRaw === 0 && unresolvedMarkers === 0
11206
+ };
11207
+ }
11208
+ /**
11209
+ * Compare-and-set under the user's queue: the row is re-read and moved only if
11210
+ * it still holds the very inline seed we planned to move, so a concurrent
11211
+ * `setup` (its seed and marker) is never overwritten. The ref is always the
11212
+ * user's own, never taken from the row.
11213
+ */
11214
+ moveRow(planned) {
11215
+ return this.serialize(planned.userId, async () => {
11216
+ const ref = this.refFor(planned.userId);
11217
+ if (this.secrets === null || ref === null) return false;
11218
+ try {
11219
+ const current = await this.find(planned.userId);
11220
+ if (current === null || current.secret !== planned.secret) return false;
11221
+ await this.secrets.put(ref, planned.secret);
11222
+ const back = (await this.secrets.resolve([ref]))[ref];
11223
+ if (back?.kind !== "value" || back.value !== planned.secret) return false;
11224
+ const next = {
11225
+ ...current,
11226
+ secret: JSON.stringify(makeSecretMarker(ref))
11227
+ };
11228
+ await this.store.update.mutate({
11229
+ collection: TOTP_COLLECTION,
11230
+ id: planned.userId,
11231
+ data: { ...next }
11232
+ });
11233
+ return true;
11234
+ } catch {
11235
+ return false;
11236
+ }
11237
+ });
11238
+ }
11239
+ async readAll() {
11240
+ const results = await this.store.query.query({ collection: TOTP_COLLECTION });
11241
+ const out = [];
11242
+ for (const r of results) try {
11243
+ out.push(parseRow(r.data));
11244
+ } catch {}
11245
+ return out;
11246
+ }
11045
11247
  async find(userId) {
11046
11248
  const results = await this.store.query.query({
11047
11249
  collection: TOTP_COLLECTION,
@@ -11539,7 +11741,10 @@ var LocalAuthAddon = class extends BaseAddon {
11539
11741
  this.apiKeyManager = new ApiKeyManager(storageAccess);
11540
11742
  this.scopedTokenManager = new ScopedTokenManager(store);
11541
11743
  this.oauthSessionManager = new OauthSessionManager(store);
11542
- this.totpManager = new TotpManager(store);
11744
+ this.totpManager = new TotpManager(store, {
11745
+ secrets: forOwner === null ? null : forOwner("system"),
11746
+ logger: this.ctx.logger
11747
+ });
11543
11748
  this.externalIdentities = new ExternalIdentityManager(store);
11544
11749
  this.loginSessions = new LoginSessionManager(store, { logger: this.ctx.logger });
11545
11750
  await this.loginSessions.load();
@@ -11781,6 +11986,10 @@ var LocalAuthAddon = class extends BaseAddon {
11781
11986
  };
11782
11987
  return this.totpManager.getStatus(input.userId);
11783
11988
  },
11989
+ migrateRawSecrets: async (input) => {
11990
+ if (!this.totpManager) throw new Error("TOTP management not available");
11991
+ return this.totpManager.migrateRawSecrets(input.apply);
11992
+ },
11784
11993
  verifyTotp: async (input) => {
11785
11994
  if (!this.totpManager) return { valid: false };
11786
11995
  return { valid: await this.totpManager.verify(input.userId, input.code) };
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-BiTCcaf-.js");
4
- const require_hub_mdns_ciao = require("../../hub-mdns-ciao-DOBTq8qr.js");
3
+ const require_dist = require("../../dist-CyIHDLCg.js");
4
+ const require_hub_mdns_ciao = require("../../hub-mdns-ciao-Da-0NEyr.js");
5
5
  let zod = require("zod");
6
6
  let node_net = require("node:net");
7
7
  let node_os = require("node:os");