@camstack/system 1.2.357 → 1.2.359

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 (70) 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/address-book.d.ts +69 -0
  35. package/dist/builtins/local-network/local-network.addon.d.ts +44 -32
  36. package/dist/builtins/local-network/local-network.addon.js +368 -55
  37. package/dist/builtins/local-network/local-network.addon.mjs +369 -54
  38. package/dist/builtins/loki-logging/index.js +1 -1
  39. package/dist/builtins/loki-logging/index.mjs +1 -1
  40. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  41. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  42. package/dist/builtins/platform-probe/index.js +1 -1
  43. package/dist/builtins/platform-probe/index.mjs +1 -1
  44. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  45. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  46. package/dist/builtins/snapshot/index.js +1 -1
  47. package/dist/builtins/snapshot/index.mjs +1 -1
  48. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  49. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  50. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  51. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  52. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  53. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/system-config/system-config.addon.js +1 -1
  55. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  56. package/dist/builtins/winston-logging/index.js +1 -1
  57. package/dist/builtins/winston-logging/index.mjs +1 -1
  58. package/dist/{child-cap-dispatch-BcRHQBKW.mjs → child-cap-dispatch-Dm1TRDCr.mjs} +2 -2
  59. package/dist/{child-cap-dispatch-wbIQJctP.js → child-cap-dispatch-t61ohDpb.js} +2 -2
  60. package/dist/{composition-sources-CeO0K2cl.js → composition-sources-CCU_KizO.js} +1 -1
  61. package/dist/{composition-sources-BBfqepyT.mjs → composition-sources-CHlfWHl6.mjs} +1 -1
  62. package/dist/{dist-CFVOy8VT.js → dist-W_X44h-s.js} +669 -121
  63. package/dist/{dist-B7WRgEZY.mjs → dist-pTfD_M-U.mjs} +628 -122
  64. package/dist/index.js +6 -141
  65. package/dist/index.mjs +4 -139
  66. package/dist/kernel/config-manager.d.ts +1 -1
  67. package/dist/kernel/config-schema.d.ts +6 -1
  68. package/dist/{retired-settings-keys-DKRjfz0U.mjs → retired-settings-keys-CK_ifret.mjs} +1 -1
  69. package/dist/{retired-settings-keys-CRlTzxaS.js → retired-settings-keys-jglEzgWR.js} +1 -1
  70. 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, In as userManagementCapability, L as ScopedTokenSchema, ar as BaseAddon, q as authProviderCapability, r as ApiKeyRecordSchema } from "../../dist-pTfD_M-U.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 };
@@ -0,0 +1,69 @@
1
+ import { ConnectionEndpoint, HubAddressBook, HubAddressFlags, HubAddressRole } from '@camstack/types';
2
+ export interface CustomAddress {
3
+ readonly baseUrl: string;
4
+ readonly label: string;
5
+ }
6
+ /** The address-book keys of the addon's stored settings. */
7
+ export interface StoredAddressSettings {
8
+ readonly customAddresses: unknown;
9
+ readonly viewerBaseUrls: readonly string[];
10
+ readonly notificationBaseUrl: string;
11
+ readonly agentBaseUrl: string;
12
+ readonly signInBaseUrl: string;
13
+ }
14
+ export interface AddressBookState {
15
+ readonly custom: readonly CustomAddress[];
16
+ readonly flags: HubAddressFlags;
17
+ }
18
+ /** Parse the stored custom list; a malformed store reads as empty, never throws. */
19
+ export declare function parseCustomAddresses(raw: unknown): readonly CustomAddress[];
20
+ /**
21
+ * The address book as it stands: stored custom addresses, plus every
22
+ * single-valued flag that names neither a detected address, a custom one nor
23
+ * the app link, promoted to a custom address (see the module doc).
24
+ */
25
+ export declare function normalizeAddressBook(stored: StoredAddressSettings, detectedUrls: ReadonlySet<string>): AddressBookState;
26
+ /** The stored-settings patch that persists `state`. */
27
+ export declare function addressBookPatch(state: AddressBookState): {
28
+ readonly customAddresses: readonly CustomAddress[];
29
+ readonly viewerBaseUrls: readonly string[];
30
+ readonly notificationBaseUrl: string;
31
+ readonly agentBaseUrl: string;
32
+ readonly signInBaseUrl: string;
33
+ };
34
+ export declare function withCustomAddress(state: AddressBookState, entry: CustomAddress): AddressBookState;
35
+ /** Drop a custom address and every flag it carried (those roles turn automatic). */
36
+ export declare function withoutCustomAddress(state: AddressBookState, baseUrl: string): AddressBookState;
37
+ /** Every role back to automatic for `role`. */
38
+ export declare function withRoleAutomatic(state: AddressBookState, role: HubAddressRole): AddressBookState;
39
+ /**
40
+ * Flag or unflag `baseUrl` for `role`. A single-valued role flagged here moves
41
+ * off every other address (it is one URL); unflagging the address that carries
42
+ * it returns the role to automatic. The app link flagged for notifications IS
43
+ * the automatic answer, stored as automatic — one representation. `viewer`
44
+ * toggles inside the set; toggling while automatic starts from the automatic
45
+ * set (`autoViewer`) so the other addresses keep racing.
46
+ */
47
+ export declare function withRoleFlag(state: AddressBookState, role: HubAddressRole, baseUrl: string, enabled: boolean, autoViewer: readonly string[]): AddressBookState;
48
+ /** What a row may be flagged with. */
49
+ export declare function eligibleRoles(endpoint: ConnectionEndpoint): readonly HubAddressRole[];
50
+ export declare const CUSTOM_ADDRESS_PRIORITY = 200;
51
+ /** A custom address as a palette entry (`kind: 'custom'`). */
52
+ export declare function customEndpoint(entry: CustomAddress): ConnectionEndpoint;
53
+ /**
54
+ * The palette a client races: the detected endpoints plus the custom
55
+ * addresses flagged `viewer` (a detected URL is never listed twice).
56
+ */
57
+ export declare function withViewerCustomEndpoints(detected: readonly ConnectionEndpoint[], state: AddressBookState): ConnectionEndpoint[];
58
+ export interface AddressBookInputs {
59
+ /** Detected endpoints in the scheme the rows are shown in. */
60
+ readonly rowsPalette: readonly ConnectionEndpoint[];
61
+ /** Detected endpoints over HTTPS — what agents and sign-in resolve over. */
62
+ readonly httpsPalette: readonly ConnectionEndpoint[];
63
+ readonly appLink: ConnectionEndpoint;
64
+ readonly appLinkHub: string | null;
65
+ }
66
+ /** Every role's answer — the resolvers from `@camstack/types`, nothing else. */
67
+ export declare function resolveRoles(state: AddressBookState, viewerPalette: readonly ConnectionEndpoint[], httpsPalette: readonly ConnectionEndpoint[]): HubAddressBook['roles'];
68
+ /** The Addresses table: app link, detected rows, then custom-only rows. */
69
+ export declare function buildAddressBook(state: AddressBookState, inputs: AddressBookInputs): HubAddressBook;
@@ -1,4 +1,5 @@
1
1
  import { AdvertisedLocalPort, ConnectionEndpoint, LocalInterface, ProviderRegistration, BaseAddon } from '@camstack/types';
2
+ import { CustomAddress } from './address-book.js';
2
3
  /**
3
4
  * Cached mesh-reachable endpoint, learned from the `mesh-network`
4
5
  * provider via the `MeshNetworkChanged` event-tail + pull-reconcile.
@@ -32,24 +33,30 @@ interface LocalNetworkConfig {
32
33
  * allowlist" (true + empty → respect operator's choice). */
33
34
  readonly bootSeeded: boolean;
34
35
  /**
35
- * The endpoint outbound artifacts (notification attachment links, deep
36
- * links) must be addressed by. Empty = AUTO: the first public candidate,
37
- * never loopback. Stored VERBATIM rather than as an index into the candidate
38
- * list — interfaces come and go, and an index would silently re-point at a
39
- * different address after a reboot.
36
+ * Role flag `notifications` (D709): the ONE address notification links are
37
+ * addressed by. Empty = automatic (the CamStack app link). Stored VERBATIM
38
+ * rather than as an index into the candidate list — interfaces come and go,
39
+ * and an index would silently re-point after a reboot. See `address-book.ts`.
40
40
  */
41
41
  readonly notificationBaseUrl: string;
42
- /**
43
- * Base URLs the SDK / viewer should race. Empty = AUTO (LAN IPv4 + public
44
- * tunnel). Stored verbatim, same contract as `notificationBaseUrl`.
45
- */
42
+ /** Role flag `viewer`: the addresses the apps race. Empty = automatic. */
46
43
  readonly viewerBaseUrls: readonly string[];
44
+ /** Operator-entered custom addresses (`address-book.ts`). Parsed on read. */
45
+ readonly customAddresses: readonly CustomAddress[];
47
46
  /** LAN HTTP listener. Default true — native apps skip the private cert. */
48
47
  readonly httpEnabled: boolean;
49
48
  /** Dedicated HTTP port. Must not equal the HTTPS listen port. */
50
49
  readonly httpPort: number;
51
50
  /** Operator-declared DNS name advertised instead of the raw LAN IP. Empty = none. */
52
51
  readonly localHostname: string;
52
+ /**
53
+ * Role flag `agents`: the ONE address agents and LAN devices pull from.
54
+ * Empty = automatic. The key is the D729 amendment's override, kept so a
55
+ * rolled-back hub reads it with the meaning it knew.
56
+ */
57
+ readonly agentBaseUrl: string;
58
+ /** Role flag `signIn`: the ONE sign-in address. Empty = automatic. Same key story. */
59
+ readonly signInBaseUrl: string;
53
60
  }
54
61
  export declare class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {
55
62
  private pollTimer;
@@ -68,27 +75,42 @@ export declare class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {
68
75
  private meshEndpoint;
69
76
  constructor();
70
77
  /**
71
- * The effective flagged notification address: the operator's explicit
72
- * choice, else AUTO — the CamStack app link (D709, operator decision
73
- * 2026-10-06; before it AUTO was the tunnel, then the mesh). The app link is
74
- * not a hub address: the NC's signed media and buttons keep their own hub
75
- * ladder when it is flagged; only viewer taps take the app form.
78
+ * The detected palettes, with the mesh reconciled once: `https` (what agents
79
+ * and sign-in resolve over, and the app link's `hub=`) and `http` (what the
80
+ * viewer race resolves over, as before).
76
81
  */
77
- private resolveNotificationEndpoint;
82
+ private palettes;
83
+ /** The stored address book, normalised against what is detected now. */
84
+ private addressState;
85
+ /** The role flags alone — they do not depend on what is detected. */
86
+ private storedFlags;
87
+ /** `getAddresses`: the Addresses table and every role's answer. */
88
+ private addressBook;
78
89
  /**
79
- * The `hub=` an app link carries: the best public HTTPS address in this
80
- * cap's own palette (tunnel, then mesh) — so it can never disagree with
81
- * what the Network page lists. See `pickAppLinkHub`.
90
+ * Apply one change to the address book and persist it, normalised (a flag
91
+ * naming an address in no row becomes a custom address in the same write).
82
92
  */
83
- private resolveAppLinkHub;
93
+ private changeAddressBook;
94
+ /** The deprecated `getHubAddresses` shape, from the role resolvers. */
95
+ private snapshotHubAddresses;
96
+ /**
97
+ * Flag / unflag `baseUrl` for `role`, or (`baseUrl: null`) return the role
98
+ * to automatic. Validated: the address must be a row (detected, custom, or
99
+ * the app link for notifications), never loopback, and `signIn` on plain
100
+ * http only when accepted. A refusal stores nothing.
101
+ */
102
+ private setRole;
84
103
  private notificationEndpointSnapshot;
85
104
  private snapshotViewerEndpoints;
86
105
  /**
87
- * Palette shown in the Network tab: LAN IPv4, stable LAN IPv6, public,
88
- * mesh. The viewer HTTP/WS race is a subset (`selectViewerEndpoints`) —
89
- * IPv6 stays in this list because ICE/WebRTC gathers it regardless.
106
+ * Palette shown to clients: LAN IPv4, stable LAN IPv6, public, mesh, and
107
+ * the custom addresses flagged `viewer` (`kind: 'custom'`). The viewer
108
+ * HTTP/WS race is a subset (`selectViewerEndpoints`) — IPv6 stays in this
109
+ * list because ICE/WebRTC gathers it regardless.
90
110
  */
91
111
  private buildAdvertisedEndpoints;
112
+ /** The DETECTED palette from the cached interface/tunnel/mesh state. */
113
+ private endpointsNow;
92
114
  protected onInitialize(): Promise<ProviderRegistration[]>;
93
115
  protected globalSettingsSchema(): import('@camstack/types').ConfigUISchema;
94
116
  protected onConfigChanged(): Promise<void>;
@@ -228,16 +250,6 @@ export interface LanHttpAdvertisement {
228
250
  */
229
251
  export declare function appLinkEndpoint(): ConnectionEndpoint;
230
252
  export declare function buildEndpoints(interfaces: readonly LocalInterface[], port: number, includeLoopback: boolean, ipv4Only: boolean, publicHostname: string, scheme?: 'http' | 'https', meshEndpoint?: MeshEndpointCache | null, lanHttp?: LanHttpAdvertisement | null): ConnectionEndpoint[];
231
- /** AUTO viewer race: LAN IPv4 + the public tunnel. Never IPv6, mesh, or loopback. */
232
- export declare function isDefaultViewerEndpoint(endpoint: ConnectionEndpoint): boolean;
233
- /**
234
- * The URLs the SDK should race for API / viewer access.
235
- *
236
- * Empty `viewerBaseUrls` = AUTO (IPv4 + public tunnel). A non-empty list is
237
- * the operator's pin; if none of those URLs are in the current palette,
238
- * fall back to AUTO rather than racing an empty set.
239
- */
240
- export declare function selectViewerEndpoints(endpoints: readonly ConnectionEndpoint[], viewerBaseUrls: readonly string[]): ConnectionEndpoint[];
241
253
  /**
242
254
  * First-boot heuristic: which addresses should the allowlist start
243
255
  * with? Includes LAN + Wi-Fi IPv4 addresses + plausible IPv6: