@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.
- 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/address-book.d.ts +69 -0
- package/dist/builtins/local-network/local-network.addon.d.ts +44 -32
- package/dist/builtins/local-network/local-network.addon.js +368 -55
- package/dist/builtins/local-network/local-network.addon.mjs +369 -54
- 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-BcRHQBKW.mjs → child-cap-dispatch-Dm1TRDCr.mjs} +2 -2
- package/dist/{child-cap-dispatch-wbIQJctP.js → child-cap-dispatch-t61ohDpb.js} +2 -2
- package/dist/{composition-sources-CeO0K2cl.js → composition-sources-CCU_KizO.js} +1 -1
- package/dist/{composition-sources-BBfqepyT.mjs → composition-sources-CHlfWHl6.mjs} +1 -1
- package/dist/{dist-CFVOy8VT.js → dist-W_X44h-s.js} +669 -121
- package/dist/{dist-B7WRgEZY.mjs → dist-pTfD_M-U.mjs} +628 -122
- package/dist/index.js +6 -141
- package/dist/index.mjs +4 -139
- package/dist/kernel/config-manager.d.ts +1 -1
- package/dist/kernel/config-schema.d.ts +6 -1
- package/dist/{retired-settings-keys-DKRjfz0U.mjs → retired-settings-keys-CK_ifret.mjs} +1 -1
- package/dist/{retired-settings-keys-CRlTzxaS.js → retired-settings-keys-jglEzgWR.js} +1 -1
- 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, 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
|
-
|
|
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 };
|
|
@@ -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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
|
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
|
-
*
|
|
80
|
-
*
|
|
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
|
|
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
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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:
|