@camstack/system 1.2.375 → 1.2.377

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