@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.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OIDC_IDENTITY_PREFIX = exports.OIDC_BRIDGE_PROVIDER_PREFIX = exports.MAGIC_LINK_BRIDGE_PROVIDER = void 0;
3
+ exports.SSO_POPUP_COMPLETE_PATH = exports.SSO_LOGIN_PATH = exports.SSO_PROFILE_PATH = exports.OIDC_IDENTITY_PREFIX = exports.OIDC_BRIDGE_PROVIDER_PREFIX = exports.MAGIC_LINK_BRIDGE_PROVIDER = void 0;
4
+ exports.popupCompletionLocation = popupCompletionLocation;
4
5
  exports.isLoginBridge = isLoginBridge;
5
6
  exports.finishSsoLogin = finishSsoLogin;
6
7
  const auth_provider_gate_js_1 = require("./auth-provider-gate.js");
@@ -22,9 +23,41 @@ exports.MAGIC_LINK_BRIDGE_PROVIDER = 'auth-magic-link';
22
23
  exports.OIDC_BRIDGE_PROVIDER_PREFIX = 'auth-oidc/';
23
24
  /** `auth-oidc` names an identity `oidc:<instance>:<sub>`. */
24
25
  exports.OIDC_IDENTITY_PREFIX = 'oidc:';
25
- /** Where a completed (or refused) link lands — the signed-in user's profile. */
26
- const PROFILE_PATH = '/admin/system/my-access';
27
- const LOGIN_PATH = '/admin/login';
26
+ /**
27
+ * Where a completed (or refused) link lands — the signed-in user's profile —
28
+ * and where a login returns. Admin-ui routes, relative to its mount: the
29
+ * route in `main.ts` prepends the hub's mount prefix. (These read `/admin/…`
30
+ * once, a path admin-ui has no route for, so both returns drew a blank page.)
31
+ */
32
+ exports.SSO_PROFILE_PATH = '/system/my-access';
33
+ exports.SSO_LOGIN_PATH = '/login';
34
+ const PROFILE_PATH = exports.SSO_PROFILE_PATH;
35
+ const LOGIN_PATH = exports.SSO_LOGIN_PATH;
36
+ /**
37
+ * The admin-ui page a POPUP sign-in lands on (D791): it hands the outcome to
38
+ * the window that opened it and closes, or — opener gone — continues to the
39
+ * page the outcome names, like a full-page return.
40
+ */
41
+ exports.SSO_POPUP_COMPLETE_PATH = '/sso-complete';
42
+ /**
43
+ * The popup form of a return `location`: the completion page, with what the
44
+ * page return carried — the login FRAGMENT (session / challenge / error) or
45
+ * the profile QUERY (link outcome) — moved into the completion page's
46
+ * fragment, after `return=login|profile`. Fragment only: a session still
47
+ * never reaches a server log, and the completion page never posts it to
48
+ * another origin.
49
+ */
50
+ function popupCompletionLocation(location) {
51
+ const hashAt = location.indexOf('#');
52
+ const pathAndQuery = hashAt === -1 ? location : location.slice(0, hashAt);
53
+ const fragment = hashAt === -1 ? '' : location.slice(hashAt + 1);
54
+ const queryAt = pathAndQuery.indexOf('?');
55
+ const path = queryAt === -1 ? pathAndQuery : pathAndQuery.slice(0, queryAt);
56
+ const query = queryAt === -1 ? '' : pathAndQuery.slice(queryAt + 1);
57
+ const target = path === exports.SSO_PROFILE_PATH ? 'profile' : 'login';
58
+ const carried = [query, fragment].filter((part) => part.length > 0).join('&');
59
+ return `${exports.SSO_POPUP_COMPLETE_PATH}#return=${target}${carried.length > 0 ? `&${carried}` : ''}`;
60
+ }
28
61
  /**
29
62
  * `(provider, subject, issuer)` of an OIDC bridge, or `null` when it is not a
30
63
  * well-formed one. A subject is unique only per issuer, so a bridge without
@@ -130,7 +163,13 @@ async function completeLogin(claims, deps) {
130
163
  }
131
164
  return signIn(await deps.minter.findUser(link.userId), claims.provider, deps);
132
165
  }
133
- async function finishSsoLogin(bridge, deps) {
166
+ async function finishSsoLogin(bridge, deps, options = {}) {
167
+ const outcome = await finishSsoReturn(bridge, deps);
168
+ if (options.popup !== true || outcome.kind !== 'redirect')
169
+ return outcome;
170
+ return redirect(popupCompletionLocation(outcome.location));
171
+ }
172
+ async function finishSsoReturn(bridge, deps) {
134
173
  if (!bridge)
135
174
  return { kind: 'error', status: 400, body: { error: 'Missing bridge token' } };
136
175
  const claims = deps.verifyBridge(bridge);
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.keychainKeyFdFromArgv = keychainKeyFdFromArgv;
37
+ exports.loadPlatformKeychainKey = loadPlatformKeychainKey;
38
+ /**
39
+ * Read the keychain master key a DESKTOP app handed this hub over an inherited
40
+ * pipe (D788): `--keychain-key-fd=<n>` names the descriptor, the parent wrote
41
+ * the key (base64) and closed it. Read once, at boot, before anything else
42
+ * can — then the descriptor is closed and the key lives only in memory, in
43
+ * the platform-key slot the keychain builtin reads.
44
+ *
45
+ * An env var was rejected: a child's environment is readable by every process
46
+ * of the same user and is inherited by every addon runner the hub forks.
47
+ */
48
+ const fs = __importStar(require("node:fs"));
49
+ const system_1 = require("@camstack/system");
50
+ const FD_ARG = '--keychain-key-fd=';
51
+ const KEY_BYTES = 32;
52
+ /** The OS keychain's name, for the Secrets page. */
53
+ function platformLabel(platform) {
54
+ switch (platform) {
55
+ case 'darwin':
56
+ return 'macOS Keychain';
57
+ case 'win32':
58
+ return 'Windows DPAPI';
59
+ default:
60
+ return 'OS secret service';
61
+ }
62
+ }
63
+ /** Parse the descriptor number out of argv; `null` when not passed. */
64
+ function keychainKeyFdFromArgv(argv) {
65
+ const arg = argv.find((a) => a.startsWith(FD_ARG));
66
+ if (arg === undefined)
67
+ return null;
68
+ const n = Number(arg.slice(FD_ARG.length));
69
+ return Number.isInteger(n) && n > 2 ? n : null;
70
+ }
71
+ function loadPlatformKeychainKey(argv = process.argv, readFd = (fd) => fs.readFileSync(fd, 'utf8'), closeFd = (fd) => fs.closeSync(fd)) {
72
+ const fd = keychainKeyFdFromArgv(argv);
73
+ if (fd === null)
74
+ return { kind: 'none' };
75
+ try {
76
+ const key = Buffer.from(readFd(fd).trim(), 'base64');
77
+ if (key.length !== KEY_BYTES)
78
+ return { kind: 'failed', error: 'not 32 bytes of key material' };
79
+ const label = platformLabel(process.platform);
80
+ (0, system_1.setPlatformKeychainKey)({ label, key });
81
+ return { kind: 'loaded', label };
82
+ }
83
+ catch (err) {
84
+ return { kind: 'failed', error: err instanceof Error ? err.message : String(err) };
85
+ }
86
+ finally {
87
+ try {
88
+ closeFd(fd);
89
+ }
90
+ catch {
91
+ /* already closed */
92
+ }
93
+ }
94
+ }
@@ -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,12 +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
- const external_link_intents_js_1 = require("./auth/external-link-intents.js");
93
93
  const sso_finish_js_1 = require("./auth/sso-finish.js");
94
94
  const sso_finish_deps_js_1 = require("./auth/sso-finish-deps.js");
95
95
  const boot_config_1 = require("./boot/boot-config");
96
+ const platform_keychain_key_1 = require("./boot/platform-keychain-key");
96
97
  const integration_id_backfill_1 = require("./boot/integration-id-backfill");
97
98
  const post_boot_service_1 = require("./boot/post-boot.service");
98
99
  const retired_public_url_env_1 = require("./boot/retired-public-url-env");
@@ -250,6 +251,15 @@ async function bootstrap() {
250
251
  // forked, so no child can read it — and warned about once a logger exists
251
252
  // (below). See `boot/retired-public-url-env.ts`.
252
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
+ }
253
263
  // hub-main's own memory, on the record. Deliberately started HERE and not in
254
264
  // the starter: the starter runs from the image's /opt closure, which only
255
265
  // changes with a new image, while this entry is what `applyServerUpdate`
@@ -831,11 +841,15 @@ async function bootstrap() {
831
841
  logFailure: (error) => ssoLogger.error('SSO finish failed', {
832
842
  meta: { error: error instanceof Error ? error.message : String(error) },
833
843
  }),
834
- }, request.headers.cookie));
844
+ }, request.headers.cookie),
845
+ // A popup sign-in (D791) returns through the admin-ui completion page.
846
+ { popup: request.query.popup === '1' });
835
847
  if (outcome.kind === 'error')
836
848
  return reply.status(outcome.status).send(outcome.body);
837
849
  reply.code(302);
838
- 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}`);
839
853
  return reply.send('');
840
854
  });
841
855
  // ── Backup archive download ─────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.481",
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.354",
41
- "@camstack/addon-agent-ui": "1.2.191",
42
- "@camstack/addon-auth": "1.2.131",
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.343",
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.376",
50
- "@camstack/types": "1.2.327",
51
- "@camstack/ui-library": "1.2.249",
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",