@camstack/server 1.2.481 → 1.2.482

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.
@@ -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`. Closing it needs the
581
- // caller identity to survive `$hub-cap-fwd`'s envelope, which today it
582
- // does not.
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
- const workerEntry = (0, system_1.udsChildLogToWorkerEntry)(childId, entry);
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, ...payload } = input;
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, input.settings ?? {}, logger);
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 = input.settings != null && Object.keys(input.settings).length > 0;
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
- return reg.getIntegrationSettings(input.id);
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 reg.setIntegrationSettings(input.id, input.settings);
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: 162 Skipped (legacy): 3
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)