@camstack/server 1.2.480 → 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.
@@ -1151,6 +1151,22 @@ class AddonRegistryService {
1151
1151
  * addon (`execution.group`) is never misclassified as `in-process`.
1152
1152
  * Falls back to the addon id when the declaration is not yet known.
1153
1153
  */
1154
+ /** The runner id hosting an addon — its `execution.group`, or itself. */
1155
+ runnerIdFor(addonId) {
1156
+ return this.childIdForAddon(addonId);
1157
+ }
1158
+ /**
1159
+ * A hub in-process keychain handle acting for `owner` — the door hub-main
1160
+ * code (the integration settings provider) uses to keep another addon's
1161
+ * values (D787). Never handed to an addon.
1162
+ */
1163
+ secretsForOwner(owner) {
1164
+ return (0, types_1.createSecretsAccess)({
1165
+ owner,
1166
+ transport: this.inProcessKeychainTransport(owner),
1167
+ eventBus: this.eventBusService,
1168
+ });
1169
+ }
1154
1170
  childIdForAddon(addonId) {
1155
1171
  const decl = this.addonEntries.get(addonId)?.declaration;
1156
1172
  return decl ? (0, types_1.resolveRunnerId)(decl, addonId) : addonId;
@@ -2934,6 +2950,9 @@ class AddonRegistryService {
2934
2950
  deps: new system_1.AddonDepsManager(dataDir, logger),
2935
2951
  kernel: {
2936
2952
  localNodeId: this.broker.nodeID,
2953
+ // The keychain accepts a grouped runner's stamp (its group id) for the
2954
+ // refs of the addons that runner hosts.
2955
+ runnerIdForAddon: (id) => this.childIdForAddon(id),
2937
2956
  storage: storageProvider ?? undefined,
2938
2957
  deviceRegistry: this.deviceRegistry ?? undefined,
2939
2958
  devices: deviceManagerApi,
@@ -2978,6 +2997,16 @@ class AddonRegistryService {
2978
2997
  return (0, types_1.createDeviceProxy)(api, binding);
2979
2998
  },
2980
2999
  settings: settingsView,
3000
+ // This builtin's keychain values (D787). In-process, so the caller is
3001
+ // STATED (no wire, no stamp) — the one place the keychain trusts a
3002
+ // claim. `forOwner` is the door a system builtin (device-manager,
3003
+ // storage-orchestrator) uses to keep another addon's values.
3004
+ secrets: (0, types_1.createSecretsAccess)({
3005
+ owner: addonId,
3006
+ transport: this.inProcessKeychainTransport(addonId),
3007
+ transportForOwner: (owner) => this.inProcessKeychainTransport(owner),
3008
+ eventBus: this.eventBusService,
3009
+ }),
2981
3010
  useCapability(capName, scope = { type: 'global' }) {
2982
3011
  return getOrCreateHandle(capName, scope, 15_000);
2983
3012
  },
@@ -2997,6 +3026,36 @@ class AddonRegistryService {
2997
3026
  };
2998
3027
  return ctx;
2999
3028
  }
3029
+ /**
3030
+ * The keychain transport of a hub in-process caller: the provider lives in
3031
+ * this process, so the call goes straight to it with the caller STATED.
3032
+ * Built per call so a keychain registered after this context still answers;
3033
+ * a missing provider throws, which `ctx.secrets` reads as `unavailable`.
3034
+ */
3035
+ inProcessKeychainTransport(owner) {
3036
+ const provider = () => {
3037
+ const p = this.capabilityRegistry.getSingleton('keychain');
3038
+ if (p === null || p === undefined)
3039
+ throw new Error('keychain provider is not registered');
3040
+ return p;
3041
+ };
3042
+ const callerAddonId = owner;
3043
+ return {
3044
+ put: (input) => {
3045
+ const stated = { ref: input.ref, value: input.value, callerAddonId };
3046
+ return provider().put(stated);
3047
+ },
3048
+ resolve: (input) => {
3049
+ const stated = { refs: [...input.refs], callerAddonId };
3050
+ return provider().resolve(stated);
3051
+ },
3052
+ delete: (input) => {
3053
+ const stated = { refs: [...input.refs], callerAddonId };
3054
+ return provider().delete(stated);
3055
+ },
3056
+ getState: () => provider().getState({}),
3057
+ };
3058
+ }
3000
3059
  /**
3001
3060
  * Per-addon disposer chain. Created lazily the first time an addon
3002
3061
  * calls `ctx.addDisposer(...)`. Drained by `restartAddon()` /
@@ -76,7 +76,13 @@ function createAddonSettingsProvider(deps) {
76
76
  if (!addon || typeof addon.updateGlobalSettings !== 'function') {
77
77
  throw new Error(`Addon "${input.addonId}" does not implement updateGlobalSettings`);
78
78
  }
79
- await addon.updateGlobalSettings(input.patch, input.nodeId);
79
+ // The admin API's write is the OPERATOR's (D787).
80
+ if (typeof addon.updateGlobalSettingsAsOperator === 'function') {
81
+ await addon.updateGlobalSettingsAsOperator(input.patch, input.nodeId);
82
+ }
83
+ else {
84
+ await addon.updateGlobalSettings(input.patch, input.nodeId);
85
+ }
80
86
  return { success: true };
81
87
  }
82
88
  return forkedUpdate(input.addonId, 'updateGlobalSettings', { patch: input.patch, ...(input.nodeId ? { nodeId: input.nodeId } : {}) }, input.nodeId);
@@ -132,6 +132,27 @@ class MoleculerService {
132
132
  setRunnerIdResolver(resolver) {
133
133
  this.runnerIdResolver = resolver;
134
134
  }
135
+ /**
136
+ * Does `nodeId` (an agent) host the runner `runnerId`? Read from what the
137
+ * node itself registered: its manifest names the addons it runs, and an
138
+ * addon's runner is its `execution.group` or itself. A node that never
139
+ * registered hosts nothing.
140
+ */
141
+ isRunnerOnNode(nodeId, runnerId) {
142
+ const nodes = this.nodeRegistry
143
+ .listNodeIds()
144
+ .filter((id) => id === nodeId || id.startsWith(`${nodeId}/`));
145
+ if (nodes.includes(`${nodeId}/${runnerId}`))
146
+ return true;
147
+ for (const id of nodes) {
148
+ for (const entry of this.nodeRegistry.getNodeManifest(id) ?? []) {
149
+ const runner = this.runnerIdResolver?.(entry.addonId) ?? entry.addonId;
150
+ if (entry.addonId === runnerId || runner === runnerId)
151
+ return true;
152
+ }
153
+ }
154
+ return false;
155
+ }
135
156
  get childRegistry() {
136
157
  return this.localChildRegistry;
137
158
  }
@@ -400,7 +421,16 @@ class MoleculerService {
400
421
  // `broker.call('$hub-cap-fwd.forward', …)` discovers it here. Without it a
401
422
  // remote agent's raw `${cap}.*` discovery 30s-deadlines (hub-local addon
402
423
  // runners expose no Moleculer service). See hub-cap-forward.ts.
403
- this.brokerSafe.createService((0, system_1.createHubCapForwardService)(onUnownedCall));
424
+ this.brokerSafe.createService((0, system_1.createHubCapForwardService)(onUnownedCall, {
425
+ // The agent stamps WHO is calling (its child's runner id) into the
426
+ // envelope (D787). Believe it only when that runner is on the agent
427
+ // the request came from — a node's registered manifest lists the
428
+ // addons it hosts, and a grouped runner answers to its group id.
429
+ isCallerOnNode: (senderNodeId, callerRunnerId) => this.isRunnerOnNode(senderNodeId, callerRunnerId),
430
+ onRejectedCaller: (senderNodeId, callerRunnerId) => logger.warn('dropped an agent caller stamp the sender does not host', {
431
+ meta: { senderNodeId, callerRunnerId },
432
+ }),
433
+ }));
404
434
  const registry = new system_1.LocalChildRegistry({
405
435
  server: (0, system_1.createLocalTransport)().createServer(nodeId),
406
436
  // This node's own id — lets the cap-call-out router serve a pin-to-self
package/dist/main.js CHANGED
@@ -87,10 +87,13 @@ const ws_slow_consumer_guard_js_1 = require("./api/trpc/ws-slow-consumer-guard.j
87
87
  const addon_route_jwt_gate_js_1 = require("./auth/addon-route-jwt-gate.js");
88
88
  const addon_route_share_gate_js_1 = require("./auth/addon-route-share-gate.js");
89
89
  const auth_rate_limit_1 = require("./auth/auth-rate-limit");
90
+ const external_link_intents_js_1 = require("./auth/external-link-intents.js");
90
91
  const session_cookie_js_1 = require("./auth/session-cookie.js");
91
92
  const session_mint_js_1 = require("./auth/session-mint.js");
92
93
  const sso_finish_js_1 = require("./auth/sso-finish.js");
94
+ const sso_finish_deps_js_1 = require("./auth/sso-finish-deps.js");
93
95
  const boot_config_1 = require("./boot/boot-config");
96
+ const platform_keychain_key_1 = require("./boot/platform-keychain-key");
94
97
  const integration_id_backfill_1 = require("./boot/integration-id-backfill");
95
98
  const post_boot_service_1 = require("./boot/post-boot.service");
96
99
  const retired_public_url_env_1 = require("./boot/retired-public-url-env");
@@ -248,6 +251,15 @@ async function bootstrap() {
248
251
  // forked, so no child can read it — and warned about once a logger exists
249
252
  // (below). See `boot/retired-public-url-env.ts`.
250
253
  const publicUrlWarning = (0, retired_public_url_env_1.dropRetiredHubAddressEnv)(process.env);
254
+ // A desktop hub's keychain master key arrives on an inherited pipe (D788).
255
+ // Read and closed before anything is forked, so no runner inherits it.
256
+ const platformKey = (0, platform_keychain_key_1.loadPlatformKeychainKey)();
257
+ if (platformKey.kind === 'loaded') {
258
+ console.log(`[keychain] master key provided by the ${platformKey.label}`);
259
+ }
260
+ else if (platformKey.kind === 'failed') {
261
+ console.warn(`[keychain] the desktop app's key pipe could not be read: ${platformKey.error}`);
262
+ }
251
263
  // hub-main's own memory, on the record. Deliberately started HERE and not in
252
264
  // the starter: the starter runs from the image's /opt closure, which only
253
265
  // changes with a new image, while this entry is what `applyServerUpdate`
@@ -523,10 +535,14 @@ async function bootstrap() {
523
535
  // minted by `auth.createHandoffCode`, redeemed by the viewer app via the
524
536
  // public `auth.redeemHandoffCode`.
525
537
  const handoffCodeService = new handoff_code_service_1.HandoffCodeService();
538
+ // Pending "link this sign-in account" intents (D782) — minted by
539
+ // `auth.beginExternalLink`, consumed once by `/api/auth/sso/finish`.
540
+ const externalLinkIntents = new external_link_intents_js_1.ExternalLinkIntents();
526
541
  const buildRouter = () => (0, trpc_router_1.buildAppRouter)({
527
542
  authService: app.get(auth_service_1.AuthService),
528
543
  shareTokenService,
529
544
  handoffCodeService,
545
+ externalLinkIntents,
530
546
  configService: config,
531
547
  featureService: app.get(feature_service_1.FeatureService),
532
548
  loggingService,
@@ -810,19 +826,30 @@ async function bootstrap() {
810
826
  // minted by THE session mint from the user record as it is now (D712) —
811
827
  // `auth/sso-finish.ts` holds the rules, this route only maps the reply.
812
828
  const ssoAuth = app.get(auth_service_1.AuthService);
829
+ const ssoLogger = app.get(logging_service_1.LoggingService).createLogger('sso-finish');
813
830
  const ssoMinter = (0, session_mint_js_1.createSessionMinter)({
814
831
  signer: ssoAuth,
815
832
  directory: () => capabilityRegistry.getSingleton('user-management'),
816
833
  });
817
834
  fastify.get('/api/auth/sso/finish', async (request, reply) => {
818
- const outcome = await (0, sso_finish_js_1.finishSsoLogin)(request.query.bridge, {
819
- verifyBridge: (token) => ssoAuth.verifySsoBridgeToken(token),
835
+ const outcome = await (0, sso_finish_js_1.finishSsoLogin)(request.query.bridge, (0, sso_finish_deps_js_1.createSsoFinishDeps)({
836
+ auth: ssoAuth,
837
+ registry: capabilityRegistry,
838
+ moleculer: app.get(moleculer_service_1.MoleculerService),
820
839
  minter: ssoMinter,
821
- });
840
+ linkIntents: externalLinkIntents,
841
+ logFailure: (error) => ssoLogger.error('SSO finish failed', {
842
+ meta: { error: error instanceof Error ? error.message : String(error) },
843
+ }),
844
+ }, request.headers.cookie),
845
+ // A popup sign-in (D791) returns through the admin-ui completion page.
846
+ { popup: request.query.popup === '1' });
822
847
  if (outcome.kind === 'error')
823
848
  return reply.status(outcome.status).send(outcome.body);
824
849
  reply.code(302);
825
- reply.header('Location', outcome.location);
850
+ // Admin-ui paths are relative to the hub's mount (HA ingress etc.).
851
+ const ssoMount = (0, spa_mount_1.parseForwardedPrefix)(request.headers['x-forwarded-prefix']) ?? '';
852
+ reply.header('Location', `${ssoMount}${outcome.location}`);
826
853
  return reply.send('');
827
854
  });
828
855
  // ── Backup archive download ─────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.480",
3
+ "version": "1.2.482",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -37,18 +37,18 @@
37
37
  ]
38
38
  },
39
39
  "dependencies": {
40
- "@camstack/addon-admin-ui": "1.2.353",
41
- "@camstack/addon-agent-ui": "1.2.190",
42
- "@camstack/addon-auth": "1.2.130",
40
+ "@camstack/addon-admin-ui": "1.2.355",
41
+ "@camstack/addon-agent-ui": "1.2.192",
42
+ "@camstack/addon-auth": "1.2.132",
43
43
  "@camstack/addon-decoder-nodeav": "1.2.121",
44
44
  "@camstack/addon-notifiers": "1.2.128",
45
- "@camstack/addon-pipeline": "1.2.345",
46
- "@camstack/addon-pipeline-orchestrator": "1.2.297",
47
- "@camstack/addon-post-analysis": "1.2.342",
45
+ "@camstack/addon-pipeline": "1.2.346",
46
+ "@camstack/addon-pipeline-orchestrator": "1.2.298",
47
+ "@camstack/addon-post-analysis": "1.2.344",
48
48
  "@camstack/sdk": "1.2.126",
49
- "@camstack/system": "1.2.375",
50
- "@camstack/types": "1.2.326",
51
- "@camstack/ui-library": "1.2.248",
49
+ "@camstack/system": "1.2.377",
50
+ "@camstack/types": "1.2.328",
51
+ "@camstack/ui-library": "1.2.250",
52
52
  "@fastify/compress": "^9.0.0",
53
53
  "@fastify/cookie": "^11.0.2",
54
54
  "@fastify/cors": "^11.2.0",