@camstack/system 1.2.357 → 1.2.358

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 (69) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/auth/user-manager.d.ts +43 -2
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/autotrack/index.js +2 -2
  11. package/dist/builtins/autotrack/index.mjs +2 -2
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  14. package/dist/builtins/calendars/calendars.addon.js +1 -1
  15. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  16. package/dist/builtins/camera-grid/index.js +1 -1
  17. package/dist/builtins/camera-grid/index.mjs +1 -1
  18. package/dist/builtins/composer/composer.addon.js +8 -8
  19. package/dist/builtins/composer/composer.addon.mjs +8 -8
  20. package/dist/builtins/console-logging/index.js +1 -1
  21. package/dist/builtins/console-logging/index.mjs +1 -1
  22. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  23. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  24. package/dist/builtins/device-manager/device-manager.addon.js +9 -9
  25. package/dist/builtins/device-manager/device-manager.addon.mjs +9 -9
  26. package/dist/builtins/hub-forwarder/index.js +1 -1
  27. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  28. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  29. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  30. package/dist/builtins/local-auth/admin-account-boot.d.ts +27 -0
  31. package/dist/builtins/local-auth/auth-schema.d.ts +3 -2
  32. package/dist/builtins/local-auth/local-auth.addon.js +281 -8
  33. package/dist/builtins/local-auth/local-auth.addon.mjs +291 -22
  34. package/dist/builtins/local-network/local-network.addon.d.ts +19 -0
  35. package/dist/builtins/local-network/local-network.addon.js +32 -2
  36. package/dist/builtins/local-network/local-network.addon.mjs +32 -2
  37. package/dist/builtins/loki-logging/index.js +1 -1
  38. package/dist/builtins/loki-logging/index.mjs +1 -1
  39. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  40. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  41. package/dist/builtins/platform-probe/index.js +1 -1
  42. package/dist/builtins/platform-probe/index.mjs +1 -1
  43. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  44. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  45. package/dist/builtins/snapshot/index.js +1 -1
  46. package/dist/builtins/snapshot/index.mjs +1 -1
  47. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  48. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  50. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  51. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  52. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  53. package/dist/builtins/system-config/system-config.addon.js +1 -1
  54. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  55. package/dist/builtins/winston-logging/index.js +1 -1
  56. package/dist/builtins/winston-logging/index.mjs +1 -1
  57. package/dist/{child-cap-dispatch-wbIQJctP.js → child-cap-dispatch-BWk99kmA.js} +2 -2
  58. package/dist/{child-cap-dispatch-BcRHQBKW.mjs → child-cap-dispatch-C2dFiu87.mjs} +2 -2
  59. package/dist/{composition-sources-CeO0K2cl.js → composition-sources-BaHPH661.js} +1 -1
  60. package/dist/{composition-sources-BBfqepyT.mjs → composition-sources-D8Hapyav.mjs} +1 -1
  61. package/dist/{dist-B7WRgEZY.mjs → dist-D3bhbMY3.mjs} +321 -65
  62. package/dist/{dist-CFVOy8VT.js → dist-DzAHit0a.js} +332 -64
  63. package/dist/index.js +6 -141
  64. package/dist/index.mjs +4 -139
  65. package/dist/kernel/config-manager.d.ts +1 -1
  66. package/dist/kernel/config-schema.d.ts +6 -1
  67. package/dist/{retired-settings-keys-DKRjfz0U.mjs → retired-settings-keys-B6AFmtlO.mjs} +1 -1
  68. package/dist/{retired-settings-keys-CRlTzxaS.js → retired-settings-keys-CkXoPjKy.js} +1 -1
  69. package/package.json +3 -3
@@ -1,8 +1,11 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { An as userManagementCapability, H as UserRecordSchema, L as ScopedTokenSchema, Qn as BaseAddon, q as authProviderCapability, r as ApiKeyRecordSchema } from "../../dist-B7WRgEZY.mjs";
2
+ import { H as UserRecordSchema, L as ScopedTokenSchema, er as BaseAddon, jn as userManagementCapability, q as authProviderCapability, r as ApiKeyRecordSchema } from "../../dist-D3bhbMY3.mjs";
3
+ import { z } from "zod";
3
4
  import * as crypto$2 from "node:crypto";
4
5
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
6
  import nodeCrypto from "crypto";
7
+ import * as path$1 from "node:path";
8
+ import * as fs from "node:fs";
6
9
  //#region src/auth/parse-record.ts
7
10
  function parseRecord(kind, schema, data) {
8
11
  try {
@@ -6700,15 +6703,44 @@ var UserManager = class {
6700
6703
  }
6701
6704
  });
6702
6705
  }
6703
- async ensureAdminExists() {
6706
+ /**
6707
+ * Seed the configured admin (`auth.adminUsername` / `auth.adminPassword`,
6708
+ * i.e. `CAMSTACK_ADMIN_USER` / `CAMSTACK_ADMIN_PASS`) — on FIRST boot only.
6709
+ *
6710
+ * The configured password is a seed, never an authority. Once any admin
6711
+ * exists, this never writes a password: the operator's own change in the
6712
+ * admin UI is the truth from then on. This method used to compare the stored
6713
+ * hash with the configured password on every boot and rewrite it when they
6714
+ * differed, so a password changed in the UI went back to the configured one
6715
+ * (`changeme` on a Mac hub and on any container without `CAMSTACK_ADMIN_PASS`)
6716
+ * at the next restart.
6717
+ *
6718
+ * `resetPassword: true` is the one explicit exception — the operator's
6719
+ * one-shot recovery request (see `admin-password-reset.ts`): the configured
6720
+ * admin's password is set back to the configured one.
6721
+ */
6722
+ async ensureAdminExists(options = {}) {
6704
6723
  const adminUsername = this.config.get("auth.adminUsername");
6705
6724
  const adminPassword = this.config.get("auth.adminPassword");
6706
- if (!adminUsername || !adminPassword) return;
6725
+ if (!adminUsername || !adminPassword) return { kind: "not-configured" };
6707
6726
  const existing = await this.findByUsername(adminUsername);
6708
6727
  if (existing) {
6709
- if (!await this.auth.comparePassword(adminPassword, existing.passwordHash)) await this.resetPassword(existing.id, adminPassword);
6710
- return;
6728
+ if (options.resetPassword === true) {
6729
+ await this.resetPassword(existing.id, adminPassword);
6730
+ return {
6731
+ kind: "reset",
6732
+ username: adminUsername
6733
+ };
6734
+ }
6735
+ return {
6736
+ kind: "exists",
6737
+ username: adminUsername
6738
+ };
6711
6739
  }
6740
+ if ((await this.listAll()).some((u) => u.isAdmin)) return {
6741
+ kind: "other-admin-exists",
6742
+ username: adminUsername
6743
+ };
6712
6744
  await this.create({
6713
6745
  username: adminUsername,
6714
6746
  password: adminPassword,
@@ -6716,6 +6748,19 @@ var UserManager = class {
6716
6748
  allowedProviders: "*",
6717
6749
  allowedDevices: {}
6718
6750
  });
6751
+ return {
6752
+ kind: "seeded",
6753
+ username: adminUsername
6754
+ };
6755
+ }
6756
+ /**
6757
+ * Does `username` still sign in with `password`? A hash comparison — the
6758
+ * password is never logged or stored. `false` when the user does not exist.
6759
+ */
6760
+ async hasPassword(username, password) {
6761
+ const user = await this.findByUsername(username);
6762
+ if (!user) return false;
6763
+ return this.auth.comparePassword(password, user.passwordHash);
6719
6764
  }
6720
6765
  }, i$2 = class extends Error {
6721
6766
  constructor(e, t) {
@@ -7395,7 +7440,7 @@ function alphabet(letters) {
7395
7440
  /**
7396
7441
  * @__NO_SIDE_EFFECTS__
7397
7442
  */
7398
- function join(separator = "") {
7443
+ function join$1(separator = "") {
7399
7444
  astr("join", separator);
7400
7445
  return {
7401
7446
  encode: (from) => {
@@ -7505,7 +7550,7 @@ function unsafeWrapper(fn) {
7505
7550
  } catch (e) {}
7506
7551
  };
7507
7552
  }
7508
- chain(radix2(4), alphabet("0123456789ABCDEF"), join(""));
7553
+ chain(radix2(4), alphabet("0123456789ABCDEF"), join$1(""));
7509
7554
  /**
7510
7555
  * base32 encoding from RFC 4648. Has padding.
7511
7556
  * RFC 4648 §6 Table 3 uses uppercase letters, and RFC 4648 §3.4 allows applications to choose
@@ -7520,17 +7565,17 @@ chain(radix2(4), alphabet("0123456789ABCDEF"), join(""));
7520
7565
  * // => Uint8Array.from([0x12, 0xab])
7521
7566
  * ```
7522
7567
  */
7523
- var base32 = /* @__PURE__ */ Object.freeze(chain(radix2(5), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"), padding(5), join("")));
7524
- chain(radix2(5), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"), join(""));
7525
- chain(radix2(5), alphabet("0123456789ABCDEFGHIJKLMNOPQRSTUV"), padding(5), join(""));
7526
- chain(radix2(5), alphabet("0123456789ABCDEFGHIJKLMNOPQRSTUV"), join(""));
7527
- chain(radix2(5), alphabet("0123456789ABCDEFGHJKMNPQRSTVWXYZ"), join(""), normalize((s) => s.toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1")));
7568
+ var base32 = /* @__PURE__ */ Object.freeze(chain(radix2(5), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"), padding(5), join$1("")));
7569
+ chain(radix2(5), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"), join$1(""));
7570
+ chain(radix2(5), alphabet("0123456789ABCDEFGHIJKLMNOPQRSTUV"), padding(5), join$1(""));
7571
+ chain(radix2(5), alphabet("0123456789ABCDEFGHIJKLMNOPQRSTUV"), join$1(""));
7572
+ chain(radix2(5), alphabet("0123456789ABCDEFGHJKMNPQRSTVWXYZ"), join$1(""), normalize((s) => s.toUpperCase().replace(/O/g, "0").replace(/[IL]/g, "1")));
7528
7573
  var hasBase64Builtin = /* @__PURE__ */ (() => typeof Uint8Array.from([]).toBase64 === "function" && typeof Uint8Array.fromBase64 === "function")();
7529
- hasBase64Builtin || chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), padding(6), join(""));
7530
- chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), join(""));
7531
- hasBase64Builtin || chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"), padding(6), join(""));
7532
- chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"), join(""));
7533
- var BECH_ALPHABET = chain(alphabet("qpzry9x8gf2tvdw0s3jn54khce6mua7l"), join(""));
7574
+ hasBase64Builtin || chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), padding(6), join$1(""));
7575
+ chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), join$1(""));
7576
+ hasBase64Builtin || chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"), padding(6), join$1(""));
7577
+ chain(radix2(6), alphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"), join$1(""));
7578
+ var BECH_ALPHABET = chain(alphabet("qpzry9x8gf2tvdw0s3jn54khce6mua7l"), join$1(""));
7534
7579
  var POLYMOD_GENERATORS = [
7535
7580
  996825010,
7536
7581
  642813549,
@@ -7624,7 +7669,7 @@ function genBech32(encoding) {
7624
7669
  }
7625
7670
  genBech32("bech32");
7626
7671
  genBech32("bech32m");
7627
- /* @__PURE__ */ (() => typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function")() || chain(radix2(4), alphabet("0123456789abcdef"), join(""), normalize((s) => {
7672
+ /* @__PURE__ */ (() => typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function")() || chain(radix2(4), alphabet("0123456789abcdef"), join$1(""), normalize((s) => {
7628
7673
  if (typeof s !== "string" || s.length % 2 !== 0) throw new TypeError(`hex.decode: expected string, got ${typeof s} with length ${s.length}`);
7629
7674
  return s.toLowerCase();
7630
7675
  }));
@@ -9023,6 +9068,204 @@ var TotpManager = class {
9023
9068
  }
9024
9069
  };
9025
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
+ /**
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.
9114
+ */
9115
+ enabled: z.boolean().default(true) });
9116
+ /**
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.
9125
+ */
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({}))
9199
+ });
9200
+ //#endregion
9201
+ //#region src/builtins/local-auth/admin-account-boot.ts
9202
+ /**
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.
9205
+ *
9206
+ * ## Why a trigger FILE, not a variable
9207
+ *
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.
9218
+ */
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 } });
9231
+ return {
9232
+ outcome,
9233
+ usesDefaultPassword
9234
+ };
9235
+ }
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
+ } });
9266
+ }
9267
+ }
9268
+ //#endregion
9026
9269
  //#region src/builtins/local-auth/auth-config-reader.ts
9027
9270
  /**
9028
9271
  * Every config path this reader can answer. Anything else resolves to
@@ -9058,8 +9301,9 @@ function narrow(value) {
9058
9301
  //#region src/builtins/local-auth/auth-schema.ts
9059
9302
  /**
9060
9303
  * @durable class=config owner=local-auth
9061
- * write="an operator creates a user, or `ensureAdminExists` seeds/re-syncs the
9062
- * configured admin at boot; updated on scope, isAdmin and password edits"
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"
9063
9307
  * retention="none — a row goes only when an operator deletes the user. A
9064
9308
  * `declareCollection` that adds a PK/UNIQUE/NOT-NULL-without-default column
9065
9309
  * REBUILDS the table: every account is wiped and only the configured admin
@@ -9667,6 +9911,23 @@ var OauthSessionManager = class {
9667
9911
  };
9668
9912
  //#endregion
9669
9913
  //#region src/builtins/local-auth/local-auth.addon.ts
9914
+ /**
9915
+ * Local Auth Addon — owns user accounts, API keys, scoped tokens,
9916
+ * and local password authentication.
9917
+ *
9918
+ * Capabilities registered:
9919
+ * - `auth-provider` (collection) — local password authentication
9920
+ * - `user-management` (singleton) — user CRUD, API keys, scoped tokens
9921
+ *
9922
+ * This addon owns the OAuth grant MACHINERY (codes, sessions, refresh) but
9923
+ * declares no `oauth-integration` client of its own, and must not start: which
9924
+ * clients exist is integration-specific knowledge and belongs to the addon that
9925
+ * owns the integration (D101). `homeassistant` moved to `homeassistant-export`;
9926
+ * `export-alexa` was always there.
9927
+ *
9928
+ * Extension: an OIDC addon can register another `auth-provider` entry.
9929
+ * The server's login flow iterates the `auth-provider` collection.
9930
+ */
9670
9931
  /** How often the auth tables are swept for dead rows. */
9671
9932
  var AUTH_REAP_INTERVAL_MS = 24 * 36e5;
9672
9933
  /**
@@ -9724,7 +9985,15 @@ var LocalAuthAddon = class extends BaseAddon {
9724
9985
  this.oauthSessionManager = new OauthSessionManager(store);
9725
9986
  this.totpManager = new TotpManager(store);
9726
9987
  try {
9727
- await this.userManager.ensureAdminExists();
9988
+ await reconcileAdminAccountAtBoot({
9989
+ users: this.userManager,
9990
+ dataDir: this.ctx.nodeDataDir,
9991
+ fs: {
9992
+ exists: (filePath) => fs.existsSync(filePath),
9993
+ remove: (filePath) => fs.rmSync(filePath, { force: true })
9994
+ },
9995
+ logger: this.ctx.logger
9996
+ });
9728
9997
  const liveUsers = await this.userManager.listAll();
9729
9998
  const liveIds = new Set(liveUsers.map((u) => u.id));
9730
9999
  const removed = await this.scopedTokenManager.cleanupOrphans(liveIds);
@@ -9976,4 +10245,4 @@ var LocalAuthAddon = class extends BaseAddon {
9976
10245
  }
9977
10246
  };
9978
10247
  //#endregion
9979
- export { LocalAuthAddon, LocalAuthAddon as default, isSettingsReadUnavailable as a, ApiKeyManager as c, SettingsReadUnavailableError as i, ScopedTokenManager as n, require_ms as o, AuthManager as r, require_safe_buffer as s, UserManager as t };
10248
+ export { LocalAuthAddon, LocalAuthAddon as default, AuthManager as a, require_ms as c, ScopedTokenManager as i, require_safe_buffer as l, bootstrapSchema as n, SettingsReadUnavailableError as o, UserManager as r, isSettingsReadUnavailable as s, DEFAULT_DATA_PATH as t, ApiKeyManager as u };
@@ -50,6 +50,19 @@ interface LocalNetworkConfig {
50
50
  readonly httpPort: number;
51
51
  /** Operator-declared DNS name advertised instead of the raw LAN IP. Empty = none. */
52
52
  readonly localHostname: string;
53
+ /**
54
+ * "Hub address for agents": the base URL agents pull addon bundles and
55
+ * models from, when auto-detection names one they cannot reach (Docker
56
+ * bridge networking, NAT, VPN). Empty = automatic. Validated on write
57
+ * (`validateHubAddressOverride`): http(s), never loopback.
58
+ */
59
+ readonly agentBaseUrl: string;
60
+ /**
61
+ * "Public address for sign-in links": magic-link e-mails, the OIDC
62
+ * callback, the WebAuthn relying party, OAuth2's hub URL. Empty =
63
+ * automatic. Same validation as `agentBaseUrl`.
64
+ */
65
+ readonly signInBaseUrl: string;
53
66
  }
54
67
  export declare class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {
55
68
  private pollTimer;
@@ -67,6 +80,12 @@ export declare class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {
67
80
  */
68
81
  private meshEndpoint;
69
82
  constructor();
83
+ /**
84
+ * Both audiences' hub address: the override when set, else the automatic
85
+ * pick over the HTTPS palette (`resolveHubAddress`). The one reading every
86
+ * consumer goes through — `getHubAddresses`.
87
+ */
88
+ private snapshotHubAddresses;
70
89
  /**
71
90
  * The effective flagged notification address: the operator's explicit
72
91
  * choice, else AUTO — the CamStack app link (D709, operator decision
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-CFVOy8VT.js");
3
+ const require_dist = require("../../dist-DzAHit0a.js");
4
4
  const require_tls = require("../../tls-CCMX7MjX.js");
5
5
  let node_os = require("node:os");
6
6
  node_os = require_chunk.__toESM(node_os);
@@ -201,10 +201,25 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
201
201
  viewerBaseUrls: [],
202
202
  httpEnabled: true,
203
203
  httpPort: require_tls.DEFAULT_LAN_HTTP_PORT,
204
- localHostname: ""
204
+ localHostname: "",
205
+ agentBaseUrl: "",
206
+ signInBaseUrl: ""
205
207
  });
206
208
  }
207
209
  /**
210
+ * Both audiences' hub address: the override when set, else the automatic
211
+ * pick over the HTTPS palette (`resolveHubAddress`). The one reading every
212
+ * consumer goes through — `getHubAddresses`.
213
+ */
214
+ async snapshotHubAddresses() {
215
+ const port = (await this.resolveAdvertisedListenPort(null)).port;
216
+ const palette = await this.buildAdvertisedEndpoints(false, false, "https", port);
217
+ return {
218
+ cluster: require_dist.resolveHubAddress(palette, "cluster", this.config.agentBaseUrl || null),
219
+ browser: require_dist.resolveHubAddress(palette, "browser", this.config.signInBaseUrl || null)
220
+ };
221
+ }
222
+ /**
208
223
  * The effective flagged notification address: the operator's explicit
209
224
  * choice, else AUTO — the CamStack app link (D709, operator decision
210
225
  * 2026-10-06; before it AUTO was the tunnel, then the mesh). The app link is
@@ -294,6 +309,21 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
294
309
  this.ctx.logger.info("local-network: notification endpoint set", { meta: { baseUrl: baseUrl ?? "(auto)" } });
295
310
  return this.notificationEndpointSnapshot();
296
311
  },
312
+ getHubAddresses: async () => this.snapshotHubAddresses(),
313
+ setHubAddress: async ({ audience, baseUrl }) => {
314
+ let stored = "";
315
+ if (baseUrl !== null && baseUrl.trim() !== "") {
316
+ const check = require_dist.validateHubAddressOverride(baseUrl);
317
+ if (!check.ok) throw new Error(check.reason);
318
+ stored = check.baseUrl;
319
+ }
320
+ await this.updateGlobalSettings(audience === "cluster" ? { agentBaseUrl: stored } : { signInBaseUrl: stored });
321
+ this.ctx.logger.info("local-network: hub address override set", { meta: {
322
+ audience,
323
+ baseUrl: stored === "" ? "(auto)" : stored
324
+ } });
325
+ return this.snapshotHubAddresses();
326
+ },
297
327
  getViewerEndpoints: async () => this.snapshotViewerEndpoints(),
298
328
  setViewerEndpoints: async ({ baseUrls }) => {
299
329
  const cleaned = [...new Set(baseUrls.filter((u) => u.length > 0))];
@@ -1,4 +1,4 @@
1
- import { Cr as EventCategory, Qn as BaseAddon, Ut as localNetworkCapability, an as pickAppLinkHub, s as CAMSTACK_APP_LINK_ORIGIN } from "../../dist-B7WRgEZY.mjs";
1
+ import { Nn as validateHubAddressOverride, Tr as EventCategory, Ut as localNetworkCapability, an as pickAppLinkHub, er as BaseAddon, hn as resolveHubAddress, s as CAMSTACK_APP_LINK_ORIGIN } from "../../dist-D3bhbMY3.mjs";
2
2
  import { P as readLanHttpState, _ as writeExtraSans, h as readTlsMode, i as storeUploadedCertificate, j as applyLanHttp, k as DEFAULT_LAN_HTTP_PORT, m as readTlsAccessStatus, n as regenerateCertificate, p as readExtraSans, r as revertToGeneratedCertificate, t as applyCertificateToListener, u as servedHttpsLeafFingerprint, y as ensureTlsCert } from "../../tls-CJHogwTl.mjs";
3
3
  import * as os from "node:os";
4
4
  import { readFileSync } from "node:fs";
@@ -198,10 +198,25 @@ var LocalNetworkAddon = class extends BaseAddon {
198
198
  viewerBaseUrls: [],
199
199
  httpEnabled: true,
200
200
  httpPort: DEFAULT_LAN_HTTP_PORT,
201
- localHostname: ""
201
+ localHostname: "",
202
+ agentBaseUrl: "",
203
+ signInBaseUrl: ""
202
204
  });
203
205
  }
204
206
  /**
207
+ * Both audiences' hub address: the override when set, else the automatic
208
+ * pick over the HTTPS palette (`resolveHubAddress`). The one reading every
209
+ * consumer goes through — `getHubAddresses`.
210
+ */
211
+ async snapshotHubAddresses() {
212
+ const port = (await this.resolveAdvertisedListenPort(null)).port;
213
+ const palette = await this.buildAdvertisedEndpoints(false, false, "https", port);
214
+ return {
215
+ cluster: resolveHubAddress(palette, "cluster", this.config.agentBaseUrl || null),
216
+ browser: resolveHubAddress(palette, "browser", this.config.signInBaseUrl || null)
217
+ };
218
+ }
219
+ /**
205
220
  * The effective flagged notification address: the operator's explicit
206
221
  * choice, else AUTO — the CamStack app link (D709, operator decision
207
222
  * 2026-10-06; before it AUTO was the tunnel, then the mesh). The app link is
@@ -291,6 +306,21 @@ var LocalNetworkAddon = class extends BaseAddon {
291
306
  this.ctx.logger.info("local-network: notification endpoint set", { meta: { baseUrl: baseUrl ?? "(auto)" } });
292
307
  return this.notificationEndpointSnapshot();
293
308
  },
309
+ getHubAddresses: async () => this.snapshotHubAddresses(),
310
+ setHubAddress: async ({ audience, baseUrl }) => {
311
+ let stored = "";
312
+ if (baseUrl !== null && baseUrl.trim() !== "") {
313
+ const check = validateHubAddressOverride(baseUrl);
314
+ if (!check.ok) throw new Error(check.reason);
315
+ stored = check.baseUrl;
316
+ }
317
+ await this.updateGlobalSettings(audience === "cluster" ? { agentBaseUrl: stored } : { signInBaseUrl: stored });
318
+ this.ctx.logger.info("local-network: hub address override set", { meta: {
319
+ audience,
320
+ baseUrl: stored === "" ? "(auto)" : stored
321
+ } });
322
+ return this.snapshotHubAddresses();
323
+ },
294
324
  getViewerEndpoints: async () => this.snapshotViewerEndpoints(),
295
325
  setViewerEndpoints: async ({ baseUrls }) => {
296
326
  const cleaned = [...new Set(baseUrls.filter((u) => u.length > 0))];
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-DzAHit0a.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { Gt as logDestinationCapability, Qn as BaseAddon } from "../../dist-B7WRgEZY.mjs";
1
+ import { Gt as logDestinationCapability, er as BaseAddon } from "../../dist-D3bhbMY3.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-DzAHit0a.js");
7
7
  const require_proc_stat = require("../../proc-stat-DL-WUOhl.js");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_os = require("node:os");
@@ -1,4 +1,4 @@
1
- import { Cr as EventCategory, Qn as BaseAddon, Zn as errMsg, Zt as metricsProviderCapability, cr as createEvent, fn as resolveBucketMs, jt as foldSnapshotByFunction, un as reducePoints } from "../../dist-B7WRgEZY.mjs";
1
+ import { $n as errMsg, Tr as EventCategory, Zt as metricsProviderCapability, er as BaseAddon, fn as resolveBucketMs, jt as foldSnapshotByFunction, un as reducePoints, ur as createEvent } from "../../dist-D3bhbMY3.mjs";
2
2
  import { a as parseProcStat, i as nodeProcReader, n as elapsedSecSince, o as parseProcStatusRss, r as lifetimeCpuPercent, t as commandFromCmdline } from "../../proc-stat-CbC55Kqi.mjs";
3
3
  import { access, readFile, readdir } from "node:fs/promises";
4
4
  import * as os from "node:os";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-DzAHit0a.js");
7
7
  let node_os = require("node:os");
8
8
  node_os = require_chunk.__toESM(node_os);
9
9
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Cr as EventCategory, Qn as BaseAddon, St as enumerateInferenceDevices, Zn as errMsg, bn as scoreRuntimes, sn as platformProbeCapability, ur as emitReadiness } from "../../dist-B7WRgEZY.mjs";
2
+ import { $n as errMsg, St as enumerateInferenceDevices, Tr as EventCategory, er as BaseAddon, fr as emitReadiness, sn as platformProbeCapability, xn as scoreRuntimes } from "../../dist-D3bhbMY3.mjs";
3
3
  import * as os from "node:os";
4
4
  import * as fs from "node:fs";
5
5
  import { execFile } from "node:child_process";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-DzAHit0a.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { Cr as EventCategory, Qn as BaseAddon } from "../../dist-B7WRgEZY.mjs";
1
+ import { Tr as EventCategory, er as BaseAddon } from "../../dist-D3bhbMY3.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-DzAHit0a.js");
7
7
  const require_snapshot_courtesy = require("../../snapshot-courtesy-D_C5-nC0.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { Cn as snapshotCapability, Cr as EventCategory, Jn as DeviceFeature, Qn as BaseAddon, X as batteryCapability, Xn as DeviceType, Xt as memoizePinnedFfmpeg, Y as bareAddonId, Zn as errMsg, a as BatteryStatusSchema, gt as deriveBatteryPresence, hr as nodePin, kn as streamQualityLabel } from "../../dist-B7WRgEZY.mjs";
1
+ import { $n as errMsg, An as streamQualityLabel, Qn as DeviceType, Tr as EventCategory, X as batteryCapability, Xn as DeviceFeature, Xt as memoizePinnedFfmpeg, Y as bareAddonId, _r as nodePin, a as BatteryStatusSchema, er as BaseAddon, gt as deriveBatteryPresence, wn as snapshotCapability } from "../../dist-D3bhbMY3.mjs";
2
2
  import { i as renderCourtesyOverlayJpeg, n as courtesyCacheKey, r as renderCourtesyJpeg } from "../../snapshot-courtesy-CyFs1M3I.mjs";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CFVOy8VT.js");
6
+ const require_dist = require("../../dist-DzAHit0a.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { At as filesystemBrowseCapability, Dn as storageProviderCapability, Qn as BaseAddon } from "../../dist-B7WRgEZY.mjs";
1
+ import { At as filesystemBrowseCapability, On as storageProviderCapability, er as BaseAddon } from "../../dist-D3bhbMY3.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
4
4
  import * as path$1 from "node:path";