@camstack/server 1.2.481 → 1.2.483
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/agent/agent-infra-capabilities.js +19 -5
- package/dist/agent/main.js +15 -4
- package/dist/api/core/cap-providers.js +49 -5
- package/dist/api/core/integration-secrets.js +60 -0
- package/dist/api/trpc/generated-cap-mounts.js +3 -1
- package/dist/api/trpc/generated-cap-routers.js +1713 -1357
- package/dist/auth/sso-finish.js +44 -5
- package/dist/boot/platform-keychain-key.js +94 -0
- package/dist/core/addon/addon-registry.service.js +59 -0
- package/dist/core/addon/addon-settings-provider.js +7 -1
- package/dist/core/moleculer/moleculer.service.js +31 -1
- package/dist/main.js +17 -3
- package/package.json +10 -10
|
@@ -40,8 +40,8 @@ exports.resolveAgentInfraCapabilities = resolveAgentInfraCapabilities;
|
|
|
40
40
|
*
|
|
41
41
|
* **To roll back:** set `CAMSTACK_AGENT_LOCAL_DATA_STORE=1` in the agent
|
|
42
42
|
* container's environment and restart it. That restores `INFRA_CAPABILITIES`
|
|
43
|
-
* EXACTLY
|
|
44
|
-
* boots its own `sqlite-settings` again and the data door resolves that engine
|
|
43
|
+
* EXACTLY (same entries, same order, same `required` flags) minus the hub-only
|
|
44
|
+
* `keychain`, which no flag brings to an agent — so the agent boots its own `sqlite-settings` again and the data door resolves that engine
|
|
45
45
|
* instead of the hub (a local engine always wins over the remote; see
|
|
46
46
|
* `data-store-dispatch.ts`). No redeploy, no revert, and the other two commits
|
|
47
47
|
* in this series stay in place: commit 1 is inert whenever a local engine
|
|
@@ -56,6 +56,21 @@ const system_1 = require("@camstack/system");
|
|
|
56
56
|
exports.AGENT_LOCAL_DATA_STORE_ENV = 'CAMSTACK_AGENT_LOCAL_DATA_STORE';
|
|
57
57
|
/** The capability an agent no longer boots. */
|
|
58
58
|
const DATA_STORE_PROVIDER = 'data-store-provider';
|
|
59
|
+
/**
|
|
60
|
+
* The keychain (D787) is the HUB's, always — whatever the rollback flag says.
|
|
61
|
+
*
|
|
62
|
+
* Booting it on an agent was the bug that locked the hub on 2026-10-08: the
|
|
63
|
+
* agent's settings door forwards to the hub, so an agent's keychain wrote the
|
|
64
|
+
* `keychain:keys` row INTO THE HUB'S DATABASE while writing the master key to
|
|
65
|
+
* the AGENT's `<dataDir>/.keychain/master.key`. The Mac agent booted first,
|
|
66
|
+
* generated the key on the Mac, and the hub's next boot found a wrapped data
|
|
67
|
+
* key and no key file under `/data` — `key-missing`, locked. An agent-local
|
|
68
|
+
* keychain also shadowed `$hub-cap-fwd` for the agent's own forked addons, so
|
|
69
|
+
* their secrets resolved against a keychain that could only ever be locked.
|
|
70
|
+
*
|
|
71
|
+
* An agent reaches the keychain the way it reaches every hub cap: forwarded.
|
|
72
|
+
*/
|
|
73
|
+
const HUB_ONLY_INFRA = new Set(['keychain']);
|
|
59
74
|
/**
|
|
60
75
|
* Truthy spellings an operator actually types into a docker `-e`. Anything
|
|
61
76
|
* else — including the empty string a declared-but-unset variable produces —
|
|
@@ -77,7 +92,6 @@ function isEnabled(raw) {
|
|
|
77
92
|
* `INFRA_CAPABILITIES` itself is never touched: the hub reads the same array.
|
|
78
93
|
*/
|
|
79
94
|
function resolveAgentInfraCapabilities(env) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return system_1.INFRA_CAPABILITIES.filter((cap) => cap.name !== DATA_STORE_PROVIDER);
|
|
95
|
+
const keepLocalStore = isEnabled(env[exports.AGENT_LOCAL_DATA_STORE_ENV]);
|
|
96
|
+
return system_1.INFRA_CAPABILITIES.filter((cap) => !HUB_ONLY_INFRA.has(cap.name) && (keepLocalStore || cap.name !== DATA_STORE_PROVIDER));
|
|
83
97
|
}
|
package/dist/agent/main.js
CHANGED
|
@@ -544,6 +544,10 @@ async function startAgent(configPath) {
|
|
|
544
544
|
// its behalf. Dropping it here would let the hub resolve the cap
|
|
545
545
|
// NAME instead — i.e. the wrapper the caller is trying to skip.
|
|
546
546
|
...(input.native === true ? { native: true } : {}),
|
|
547
|
+
// WHO is calling: this agent's registry stamped it off the child
|
|
548
|
+
// connection the call arrived on. The hub checks it against this
|
|
549
|
+
// node and hands it to owner-checked caps (the keychain, D787).
|
|
550
|
+
...(input.callerAddonId !== undefined ? { callerAddonId: input.callerAddonId } : {}),
|
|
547
551
|
}, { timeout: 60_000 }),
|
|
548
552
|
logger: { warn: (msg) => consoleLogger.warn(`[uds-fallback] ${msg}`) },
|
|
549
553
|
});
|
|
@@ -577,9 +581,9 @@ async function startAgent(configPath) {
|
|
|
577
581
|
// hub-main; recording in the agent would produce a map nobody ever reads,
|
|
578
582
|
// which is exactly the shape that made this graph return `[]` for
|
|
579
583
|
// months. An agent's addon traffic is a KNOWN blind spot of the graph —
|
|
580
|
-
// see `LocalChildRegistryOptions.capUsageObserver`.
|
|
581
|
-
//
|
|
582
|
-
// does
|
|
584
|
+
// see `LocalChildRegistryOptions.capUsageObserver`. The caller identity
|
|
585
|
+
// now survives `$hub-cap-fwd`'s envelope (D787, for the keychain), so
|
|
586
|
+
// the hub could record these edges; nothing does yet.
|
|
583
587
|
});
|
|
584
588
|
await agentUdsRegistry.start();
|
|
585
589
|
// E1: apply child manifest + cleanup from the UDS lifecycle (agent-local children).
|
|
@@ -613,7 +617,14 @@ async function startAgent(configPath) {
|
|
|
613
617
|
// best-effort semantic as `HubForwarderDestination.forward`. Phase F will
|
|
614
618
|
// retire the broker path once every log source emits over UDS end-to-end.
|
|
615
619
|
agentUdsRegistry.onChildLog((childId, entry) => {
|
|
616
|
-
|
|
620
|
+
// Key names and URL credentials redacted before the line leaves this
|
|
621
|
+
// node (D787 §8). The child already scrubbed the values it resolved.
|
|
622
|
+
const raw = (0, system_1.udsChildLogToWorkerEntry)(childId, entry);
|
|
623
|
+
const workerEntry = {
|
|
624
|
+
...raw,
|
|
625
|
+
message: (0, system_1.redactLogText)(raw.message),
|
|
626
|
+
...(raw.meta !== undefined ? { meta: (0, system_1.redactLogMeta)(raw.meta) } : {}),
|
|
627
|
+
};
|
|
617
628
|
broker.call('log-receiver.ingest', workerEntry).catch(() => {
|
|
618
629
|
// Hub unreachable or not yet discovered — silently drop.
|
|
619
630
|
// HubForwarderDestination handles the agent's own buffered logs;
|
|
@@ -70,6 +70,7 @@ exports.buildNodesProvider = buildNodesProvider;
|
|
|
70
70
|
exports.buildIntegrationsProvider = buildIntegrationsProvider;
|
|
71
71
|
exports.dispatchCustomAction = dispatchCustomAction;
|
|
72
72
|
exports.buildAddonsProvider = buildAddonsProvider;
|
|
73
|
+
const integration_secrets_1 = require("./integration-secrets");
|
|
73
74
|
const node_crypto_1 = require("node:crypto");
|
|
74
75
|
const os = __importStar(require("node:os"));
|
|
75
76
|
const system_1 = require("@camstack/system");
|
|
@@ -685,6 +686,14 @@ function buildNodesProvider(agentRegistry, moleculer, addonRegistry, getNodeRoot
|
|
|
685
686
|
},
|
|
686
687
|
};
|
|
687
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* The keychain handle a hub door uses for an integration's addon (D787), or
|
|
691
|
+
* `null` when this registry predates the keychain (a test double, an older
|
|
692
|
+
* kernel) — the door then stores the settings as it always did.
|
|
693
|
+
*/
|
|
694
|
+
function ownerSecrets(ar, owner) {
|
|
695
|
+
return typeof ar.secretsForOwner === 'function' ? ar.secretsForOwner(owner) : null;
|
|
696
|
+
}
|
|
688
697
|
function requireIntegrationRegistry(ar) {
|
|
689
698
|
const reg = ar.getIntegrationRegistry();
|
|
690
699
|
if (!reg)
|
|
@@ -845,7 +854,10 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
845
854
|
},
|
|
846
855
|
getByAddonId: async (input) => requireIntegrationRegistry(ar).getIntegrationByAddonId(input.addonId),
|
|
847
856
|
create: async (input) => {
|
|
848
|
-
const { skipRestart, ...
|
|
857
|
+
const { skipRestart, secretKeys, settings, ...rest } = input;
|
|
858
|
+
// Settings are written AFTER the row exists: a secret's ref is scoped
|
|
859
|
+
// to the integration id (D787).
|
|
860
|
+
const payload = rest;
|
|
849
861
|
const reg = requireIntegrationRegistry(ar);
|
|
850
862
|
logger.info('request', {
|
|
851
863
|
tags: { addonId: input.addonId },
|
|
@@ -872,7 +884,7 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
872
884
|
// those would make every integration lacking the hook uncreatable, and
|
|
873
885
|
// would fail a good create on a flaky network. They pass, and they SAY SO
|
|
874
886
|
// in the log: silence here would read as "validated".
|
|
875
|
-
const verdict = await runConnectionTest(ar, input.addonId,
|
|
887
|
+
const verdict = await runConnectionTest(ar, input.addonId, settings ?? {}, logger);
|
|
876
888
|
if (verdict.status === 'rejected') {
|
|
877
889
|
logger.warn('rejected — connection test refused the settings', {
|
|
878
890
|
tags: { addonId: input.addonId },
|
|
@@ -896,11 +908,25 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
896
908
|
});
|
|
897
909
|
}
|
|
898
910
|
const integration = await reg.createIntegration(payload);
|
|
911
|
+
if (settings !== undefined && Object.keys(settings).length > 0) {
|
|
912
|
+
await (0, integration_secrets_1.writeIntegrationSettings)({
|
|
913
|
+
registry: reg,
|
|
914
|
+
secrets: ownerSecrets(ar, integration.addonId),
|
|
915
|
+
onOrphaned: (refs) => logger.warn('dropped keychain refs could not be deleted', {
|
|
916
|
+
meta: { refs: [...refs] },
|
|
917
|
+
}),
|
|
918
|
+
}, {
|
|
919
|
+
integrationId: integration.id,
|
|
920
|
+
settings,
|
|
921
|
+
secretKeys: secretKeys ?? [],
|
|
922
|
+
operatorWrite: true,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
899
925
|
logger.info('persisted', {
|
|
900
926
|
tags: { integrationId: integration.id, addonId: integration.addonId },
|
|
901
927
|
meta: { phase: 'create' },
|
|
902
928
|
});
|
|
903
|
-
const hasSettings =
|
|
929
|
+
const hasSettings = settings != null && Object.keys(settings).length > 0;
|
|
904
930
|
if (!skipRestart && hasSettings) {
|
|
905
931
|
logger.info('settings present — restarting addon', {
|
|
906
932
|
tags: { addonId: input.addonId },
|
|
@@ -1086,7 +1112,13 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
1086
1112
|
const integration = await reg.getIntegration(input.id);
|
|
1087
1113
|
if (!integration)
|
|
1088
1114
|
throw new Error(`Integration "${input.id}" not found`);
|
|
1089
|
-
|
|
1115
|
+
// The integration's own addon (the caller the transport stamped) reads
|
|
1116
|
+
// its secrets resolved; anyone else — the admin form — reads "set"
|
|
1117
|
+
// views, never a value (D787, D789).
|
|
1118
|
+
const caller = Reflect.get(input, 'callerAddonId');
|
|
1119
|
+
const forOwner = typeof caller === 'string' &&
|
|
1120
|
+
(caller === integration.addonId || ar.runnerIdFor(integration.addonId) === caller);
|
|
1121
|
+
return (0, integration_secrets_1.readIntegrationSettings)({ registry: reg, secrets: forOwner ? ownerSecrets(ar, integration.addonId) : null }, { integrationId: input.id, forOwner });
|
|
1090
1122
|
},
|
|
1091
1123
|
setSettings: async (input) => {
|
|
1092
1124
|
const reg = requireIntegrationRegistry(ar);
|
|
@@ -1103,7 +1135,19 @@ function buildIntegrationsProvider(ar, eb, loggingService, capabilityRegistry) {
|
|
|
1103
1135
|
tags: { integrationId: input.id, addonId: integration.addonId },
|
|
1104
1136
|
meta: { phase: 'setSettings', keys: settingsKeys },
|
|
1105
1137
|
});
|
|
1106
|
-
await
|
|
1138
|
+
await (0, integration_secrets_1.writeIntegrationSettings)({
|
|
1139
|
+
registry: reg,
|
|
1140
|
+
secrets: ownerSecrets(ar, integration.addonId),
|
|
1141
|
+
onOrphaned: (refs) => logger.warn('dropped keychain refs could not be deleted', {
|
|
1142
|
+
meta: { refs: [...refs] },
|
|
1143
|
+
}),
|
|
1144
|
+
}, {
|
|
1145
|
+
integrationId: input.id,
|
|
1146
|
+
settings: input.settings,
|
|
1147
|
+
secretKeys: input.secretKeys ?? [],
|
|
1148
|
+
// The integration's own addon writing back is not the operator.
|
|
1149
|
+
operatorWrite: typeof Reflect.get(input, 'callerAddonId') !== 'string',
|
|
1150
|
+
});
|
|
1107
1151
|
logger.info('persisted — restarting addon', {
|
|
1108
1152
|
tags: { addonId: integration.addonId },
|
|
1109
1153
|
meta: { phase: 'setSettings' },
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.writeIntegrationSettings = writeIntegrationSettings;
|
|
4
|
+
exports.readIntegrationSettings = readIntegrationSettings;
|
|
5
|
+
/**
|
|
6
|
+
* The integration-settings door of the keychain (D787). An integration's
|
|
7
|
+
* settings (a Wyze password, a PetKit account password) live in the
|
|
8
|
+
* `integration_settings` table, written by `integrations.create` /
|
|
9
|
+
* `setSettings` and read back by the integration's own addon through
|
|
10
|
+
* `integrations.getSettings` — and by the admin form through the same method.
|
|
11
|
+
*
|
|
12
|
+
* - WRITE: the keys the form's schema declares secret (`secretKeys`, by type)
|
|
13
|
+
* and every key that already holds a marker go through the keychain save
|
|
14
|
+
* path, OWNED by the integration's addon (`integration/<id>` scope). A value
|
|
15
|
+
* equal to an inline pre-keychain one stays inline; `''` keeps.
|
|
16
|
+
* - READ, by the owning addon (the caller the transport stamped): markers
|
|
17
|
+
* resolve to values. READ, by anyone else (the admin form): markers become
|
|
18
|
+
* their "set" views — the value never leaves the hub.
|
|
19
|
+
*/
|
|
20
|
+
const types_1 = require("@camstack/types");
|
|
21
|
+
async function writeIntegrationSettings(deps, input) {
|
|
22
|
+
if (deps.secrets === null) {
|
|
23
|
+
return deps.registry.setIntegrationSettings(input.integrationId, input.settings);
|
|
24
|
+
}
|
|
25
|
+
const stored = (await deps.registry.getIntegrationSettings(input.integrationId)) ?? {};
|
|
26
|
+
const markerKeys = Object.entries(stored)
|
|
27
|
+
.filter(([, v]) => (0, types_1.isSecretMarker)(v))
|
|
28
|
+
.map(([k]) => k);
|
|
29
|
+
const tree = (0, types_1.secretTreeFromPaths)([...new Set([...input.secretKeys, ...markerKeys])]);
|
|
30
|
+
if (tree.size === 0) {
|
|
31
|
+
return deps.registry.setIntegrationSettings(input.integrationId, input.settings);
|
|
32
|
+
}
|
|
33
|
+
const result = await (0, types_1.transformSecretWrite)({
|
|
34
|
+
tree,
|
|
35
|
+
secrets: deps.secrets,
|
|
36
|
+
patch: input.settings,
|
|
37
|
+
stored,
|
|
38
|
+
scope: { kind: 'integration', integrationId: input.integrationId },
|
|
39
|
+
operatorWrite: input.operatorWrite === true,
|
|
40
|
+
});
|
|
41
|
+
if (Object.keys(result.patch).length > 0) {
|
|
42
|
+
await deps.registry.setIntegrationSettings(input.integrationId, result.patch);
|
|
43
|
+
}
|
|
44
|
+
const { orphaned } = await result.afterWrite();
|
|
45
|
+
if (orphaned.length > 0)
|
|
46
|
+
deps.onOrphaned?.(orphaned);
|
|
47
|
+
}
|
|
48
|
+
async function readIntegrationSettings(deps, input) {
|
|
49
|
+
const stored = await deps.registry.getIntegrationSettings(input.integrationId);
|
|
50
|
+
if (input.forOwner) {
|
|
51
|
+
if (deps.secrets === null)
|
|
52
|
+
return stored;
|
|
53
|
+
return (await (0, types_1.resolveSecretMarkers)(stored, deps.secrets)).value;
|
|
54
|
+
}
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
57
|
+
out[key] = (0, types_1.isSecretMarker)(value) ? (0, types_1.toSecretFieldView)(value) : value;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// AUTO-GENERATED by scripts/generate-cap-mounts.ts — DO NOT EDIT
|
|
3
3
|
// Re-run: npx tsx scripts/generate-cap-mounts.ts
|
|
4
4
|
//
|
|
5
|
-
// Mounted:
|
|
5
|
+
// Mounted: 164 Skipped (legacy): 3
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.LEGACY_SHAPE_SKIP = void 0;
|
|
8
8
|
exports.mountAllCaps = mountAllCaps;
|
|
@@ -269,6 +269,8 @@ function mountAllCaps(services) {
|
|
|
269
269
|
imageSettings: (0, generated_cap_routers_js_1.createCapRouter_imageSettings)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'image-settings'), remoteCapProxy),
|
|
270
270
|
integrations: (0, generated_cap_routers_js_1.createCapRouter_integrations)((_ctx) => reg?.getSingleton('integrations') ?? null, remoteCapProxy),
|
|
271
271
|
intercom: (0, generated_cap_routers_js_1.createCapRouter_intercom)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'intercom'), remoteCapProxy),
|
|
272
|
+
keychain: (0, generated_cap_routers_js_1.createCapRouter_keychain)((_ctx) => reg?.getSingleton('keychain') ?? null, remoteCapProxy),
|
|
273
|
+
keychainAdmin: (0, generated_cap_routers_js_1.createCapRouter_keychainAdmin)((_ctx) => reg?.getSingleton('keychain-admin') ?? null, remoteCapProxy),
|
|
272
274
|
lawnMowerControl: (0, generated_cap_routers_js_1.createCapRouter_lawnMowerControl)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'lawn-mower-control'), remoteCapProxy),
|
|
273
275
|
llm: (0, generated_cap_routers_js_1.createCapRouter_llm)((_ctx, addonId) => {
|
|
274
276
|
if (!reg)
|