@camstack/system 1.2.356 → 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.
- package/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/auth/user-manager.d.ts +43 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +2 -2
- package/dist/builtins/autotrack/index.mjs +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +8 -8
- package/dist/builtins/composer/composer.addon.mjs +8 -8
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +9 -9
- package/dist/builtins/device-manager/device-manager.addon.mjs +9 -9
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/admin-account-boot.d.ts +27 -0
- package/dist/builtins/local-auth/auth-schema.d.ts +3 -2
- package/dist/builtins/local-auth/local-auth.addon.js +281 -8
- package/dist/builtins/local-auth/local-auth.addon.mjs +291 -22
- package/dist/builtins/local-network/local-network.addon.d.ts +26 -0
- package/dist/builtins/local-network/local-network.addon.js +74 -25
- package/dist/builtins/local-network/local-network.addon.mjs +74 -25
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{child-cap-dispatch-wbIQJctP.js → child-cap-dispatch-BWk99kmA.js} +2 -2
- package/dist/{child-cap-dispatch-BcRHQBKW.mjs → child-cap-dispatch-C2dFiu87.mjs} +2 -2
- package/dist/{composition-sources-CeO0K2cl.js → composition-sources-BaHPH661.js} +1 -1
- package/dist/{composition-sources-BBfqepyT.mjs → composition-sources-D8Hapyav.mjs} +1 -1
- package/dist/{dist-B7WRgEZY.mjs → dist-D3bhbMY3.mjs} +321 -65
- package/dist/{dist-CFVOy8VT.js → dist-DzAHit0a.js} +332 -64
- package/dist/index.js +19 -143
- package/dist/index.mjs +6 -142
- package/dist/kernel/config-manager.d.ts +1 -1
- package/dist/kernel/config-schema.d.ts +6 -3
- package/dist/{retired-settings-keys-DKRjfz0U.mjs → retired-settings-keys-B6AFmtlO.mjs} +1 -1
- package/dist/{retired-settings-keys-CRlTzxaS.js → retired-settings-keys-CkXoPjKy.js} +1 -1
- package/dist/tls/boot-tls.d.ts +29 -0
- package/dist/tls/https-listener.d.ts +30 -0
- package/dist/tls/index.d.ts +4 -1
- package/dist/tls/tls-actions.d.ts +29 -0
- package/dist/tls/tls-status.d.ts +12 -1
- package/dist/{tls-ue_Xefmv.js → tls-CCMX7MjX.js} +319 -54
- package/dist/{tls-DTlVWwAI.mjs → tls-CJHogwTl.mjs} +246 -53
- 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 {
|
|
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
|
-
|
|
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 (
|
|
6710
|
-
|
|
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
|
|
9062
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
@@ -120,6 +139,13 @@ export declare class LocalNetworkAddon extends BaseAddon<LocalNetworkConfig> {
|
|
|
120
139
|
private resolvedHttpPort;
|
|
121
140
|
private tlsDataDir;
|
|
122
141
|
private snapshotTls;
|
|
142
|
+
/**
|
|
143
|
+
* Hot-swap the HTTPS listener onto the pair now on disk. A failure is not
|
|
144
|
+
* thrown — the files are already written and the status reports
|
|
145
|
+
* `restartRequired` from the served-vs-disk fingerprints — but it is logged:
|
|
146
|
+
* a certificate change that did not reach the listener must say so.
|
|
147
|
+
*/
|
|
148
|
+
private putCertificateOnListener;
|
|
123
149
|
/**
|
|
124
150
|
* Bind or unbind the LAN HTTP sibling from the addon's own settings, and
|
|
125
151
|
* keep the leaf SAN in sync with `localHostname`. This is the settings
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
4
|
-
const require_tls = require("../../tls-
|
|
5
|
-
let node_fs_promises = require("node:fs/promises");
|
|
6
|
-
let node_path = require("node:path");
|
|
3
|
+
const require_dist = require("../../dist-DzAHit0a.js");
|
|
4
|
+
const require_tls = require("../../tls-CCMX7MjX.js");
|
|
7
5
|
let node_os = require("node:os");
|
|
8
6
|
node_os = require_chunk.__toESM(node_os);
|
|
9
7
|
let node_fs = require("node:fs");
|
|
@@ -203,10 +201,25 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
203
201
|
viewerBaseUrls: [],
|
|
204
202
|
httpEnabled: true,
|
|
205
203
|
httpPort: require_tls.DEFAULT_LAN_HTTP_PORT,
|
|
206
|
-
localHostname: ""
|
|
204
|
+
localHostname: "",
|
|
205
|
+
agentBaseUrl: "",
|
|
206
|
+
signInBaseUrl: ""
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
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
|
+
/**
|
|
210
223
|
* The effective flagged notification address: the operator's explicit
|
|
211
224
|
* choice, else AUTO — the CamStack app link (D709, operator decision
|
|
212
225
|
* 2026-10-06; before it AUTO was the tunnel, then the mesh). The app link is
|
|
@@ -296,6 +309,21 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
296
309
|
this.ctx.logger.info("local-network: notification endpoint set", { meta: { baseUrl: baseUrl ?? "(auto)" } });
|
|
297
310
|
return this.notificationEndpointSnapshot();
|
|
298
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
|
+
},
|
|
299
327
|
getViewerEndpoints: async () => this.snapshotViewerEndpoints(),
|
|
300
328
|
setViewerEndpoints: async ({ baseUrls }) => {
|
|
301
329
|
const cleaned = [...new Set(baseUrls.filter((u) => u.length > 0))];
|
|
@@ -326,32 +354,30 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
326
354
|
} });
|
|
327
355
|
return { success: true };
|
|
328
356
|
},
|
|
329
|
-
getTlsStatus: async () => this.snapshotTls(
|
|
357
|
+
getTlsStatus: async () => this.snapshotTls(),
|
|
330
358
|
regenerateCertificate: async (_input) => {
|
|
331
|
-
if (require_tls.readTlsMode(this.tlsDataDir()) === "uploaded") throw new Error("Cannot regenerate an uploaded certificate — revert to generated first.");
|
|
332
359
|
const extraSans = [...require_tls.readExtraSans(this.tlsDataDir())];
|
|
333
|
-
await require_tls.
|
|
334
|
-
|
|
360
|
+
await require_tls.regenerateCertificate(this.tlsDataDir(), { extraSans });
|
|
361
|
+
this.putCertificateOnListener("regenerate");
|
|
362
|
+
return this.snapshotTls();
|
|
335
363
|
},
|
|
336
364
|
uploadCertificate: async ({ certPem, keyPem, caPem }) => {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
require_tls.writeTlsMode(this.tlsDataDir(), "uploaded");
|
|
345
|
-
return this.snapshotTls(true);
|
|
365
|
+
await require_tls.storeUploadedCertificate(this.tlsDataDir(), {
|
|
366
|
+
certPem,
|
|
367
|
+
keyPem,
|
|
368
|
+
caPem
|
|
369
|
+
});
|
|
370
|
+
this.putCertificateOnListener("upload");
|
|
371
|
+
return this.snapshotTls();
|
|
346
372
|
},
|
|
347
373
|
downloadCa: async () => {
|
|
348
|
-
return { pem: this.snapshotTls(
|
|
374
|
+
return { pem: this.snapshotTls().caCertPem ?? "" };
|
|
349
375
|
},
|
|
350
376
|
revertToGeneratedCertificate: async () => {
|
|
351
|
-
require_tls.writeTlsMode(this.tlsDataDir(), "generated");
|
|
352
377
|
const extraSans = [...require_tls.readExtraSans(this.tlsDataDir())];
|
|
353
|
-
await require_tls.
|
|
354
|
-
|
|
378
|
+
await require_tls.revertToGeneratedCertificate(this.tlsDataDir(), { extraSans });
|
|
379
|
+
this.putCertificateOnListener("revert");
|
|
380
|
+
return this.snapshotTls();
|
|
355
381
|
}
|
|
356
382
|
};
|
|
357
383
|
this.lastSnapshotKey = snapshotKey(this.enumerate());
|
|
@@ -510,14 +536,35 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
510
536
|
tlsDataDir() {
|
|
511
537
|
return this.ctx.nodeDataDir ?? this.ctx.dataDir;
|
|
512
538
|
}
|
|
513
|
-
snapshotTls(
|
|
514
|
-
const status = require_tls.readTlsAccessStatus(this.tlsDataDir(),
|
|
539
|
+
snapshotTls() {
|
|
540
|
+
const status = require_tls.readTlsAccessStatus(this.tlsDataDir(), require_tls.servedHttpsLeafFingerprint());
|
|
515
541
|
return {
|
|
516
542
|
...status,
|
|
517
543
|
sans: [...status.sans]
|
|
518
544
|
};
|
|
519
545
|
}
|
|
520
546
|
/**
|
|
547
|
+
* Hot-swap the HTTPS listener onto the pair now on disk. A failure is not
|
|
548
|
+
* thrown — the files are already written and the status reports
|
|
549
|
+
* `restartRequired` from the served-vs-disk fingerprints — but it is logged:
|
|
550
|
+
* a certificate change that did not reach the listener must say so.
|
|
551
|
+
*/
|
|
552
|
+
putCertificateOnListener(action) {
|
|
553
|
+
const outcome = require_tls.applyCertificateToListener(this.tlsDataDir());
|
|
554
|
+
if (outcome.kind === "reloaded") {
|
|
555
|
+
this.ctx.logger.info("local-network: HTTPS listener now serves the new certificate", { meta: {
|
|
556
|
+
action,
|
|
557
|
+
leafFingerprintSha256: outcome.servedLeafFingerprint
|
|
558
|
+
} });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
this.ctx.logger.warn("local-network: certificate changed on disk but NOT on the listener", { meta: {
|
|
562
|
+
action,
|
|
563
|
+
reason: outcome.kind,
|
|
564
|
+
...outcome.kind === "failed" ? { error: outcome.error } : {}
|
|
565
|
+
} });
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
521
568
|
* Bind or unbind the LAN HTTP sibling from the addon's own settings, and
|
|
522
569
|
* keep the leaf SAN in sync with `localHostname`. This is the settings
|
|
523
570
|
* side-effect path — there is no second persist API.
|
|
@@ -536,7 +583,9 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
|
|
|
536
583
|
});
|
|
537
584
|
if (hostname !== "") {
|
|
538
585
|
require_tls.writeExtraSans(this.tlsDataDir(), [hostname]);
|
|
539
|
-
if (require_tls.readTlsMode(this.tlsDataDir()) === "generated")
|
|
586
|
+
if (require_tls.readTlsMode(this.tlsDataDir()) === "generated") {
|
|
587
|
+
if ((await require_tls.ensureTlsCert(this.tlsDataDir(), { extraSans: [hostname] })).generated) this.putCertificateOnListener("hostname");
|
|
588
|
+
}
|
|
540
589
|
} else require_tls.writeExtraSans(this.tlsDataDir(), []);
|
|
541
590
|
}
|
|
542
591
|
/**
|