@camstack/server 1.2.486 → 1.2.487

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.
Files changed (36) hide show
  1. package/dist/agent/agent-mesh-join.js +80 -0
  2. package/dist/agent/main.js +61 -2
  3. package/dist/api/auth-whoami.js +1 -1
  4. package/dist/api/core/auth-admin-tokens.procedures.js +138 -0
  5. package/dist/api/core/auth-limits.js +77 -0
  6. package/dist/api/core/auth-reauth.procedures.js +108 -0
  7. package/dist/api/core/auth-sessions.procedures.js +166 -0
  8. package/dist/api/core/auth.router.js +281 -80
  9. package/dist/api/health/adaptive-probe.routes.js +1 -1
  10. package/dist/api/trpc/core-cap-bridge.js +47 -5
  11. package/dist/api/trpc/generated-cap-mounts.js +19 -1
  12. package/dist/api/trpc/generated-cap-routers.js +512 -205
  13. package/dist/api/trpc/trpc.context.js +86 -34
  14. package/dist/api/trpc/trpc.middleware.js +8 -11
  15. package/dist/api/trpc/trpc.router.js +6 -1
  16. package/dist/auth/auth-network-policy.js +75 -0
  17. package/dist/auth/auth-rate-limit.js +25 -20
  18. package/dist/auth/client-address.js +68 -0
  19. package/dist/auth/external-link-intents.js +12 -3
  20. package/dist/auth/hub-sessions.js +74 -0
  21. package/dist/auth/native-link-completions.js +68 -0
  22. package/dist/auth/native-return.js +33 -0
  23. package/dist/auth/session-lifetimes.js +46 -0
  24. package/dist/auth/session-mint.js +110 -13
  25. package/dist/auth/sso-finish-deps.js +56 -1
  26. package/dist/auth/sso-finish.js +132 -17
  27. package/dist/auth/sso-routes.js +95 -0
  28. package/dist/auth/used-bridge-ids.js +52 -0
  29. package/dist/core/addon/addon-registry.service.js +5 -0
  30. package/dist/core/agent/agent-registry.service.js +10 -0
  31. package/dist/core/auth/auth.service.js +50 -1
  32. package/dist/core/auth/handoff-code.service.js +9 -3
  33. package/dist/core/moleculer/hub-mesh-guard.js +132 -0
  34. package/dist/core/moleculer/moleculer.service.js +33 -0
  35. package/dist/main.js +92 -28
  36. package/package.json +13 -13
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UsedBridgeIds = void 0;
4
+ /**
5
+ * The `jti`s of login bridges already swapped for a session or a link.
6
+ *
7
+ * A bridge lives 5 minutes; until it expires a second presentation is refused.
8
+ * Entries are kept for {@link RETENTION_MS} (longer than any bridge), and the
9
+ * set is bounded — only a bridge that verified AND matched its browser's
10
+ * state cookie is ever recorded, so filling it needs that many real sign-ins.
11
+ *
12
+ * In memory: a hub restart within a bridge's 5 minutes forgets it, and the
13
+ * browser binding (the bridge is useless without the state cookie of the
14
+ * browser that started the sign-in) is what still stands then.
15
+ */
16
+ const RETENTION_MS = 10 * 60_000;
17
+ const DEFAULT_MAX_ENTRIES = 10_000;
18
+ class UsedBridgeIds {
19
+ used = new Map();
20
+ now;
21
+ maxEntries;
22
+ constructor(opts = {}) {
23
+ this.now = opts.now ?? Date.now;
24
+ this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
25
+ }
26
+ /** Record `jti`; `true` the first time, `false` for every replay. */
27
+ consume(jti) {
28
+ const at = this.now();
29
+ this.prune(at);
30
+ if (this.used.has(jti))
31
+ return false;
32
+ while (this.used.size >= this.maxEntries) {
33
+ const oldest = this.used.keys().next();
34
+ if (oldest.done)
35
+ break;
36
+ this.used.delete(oldest.value);
37
+ }
38
+ this.used.set(jti, at + RETENTION_MS);
39
+ return true;
40
+ }
41
+ size() {
42
+ return this.used.size;
43
+ }
44
+ prune(at) {
45
+ for (const [jti, until] of this.used) {
46
+ if (until > at)
47
+ break; // insertion order = expiry order
48
+ this.used.delete(jti);
49
+ }
50
+ }
51
+ }
52
+ exports.UsedBridgeIds = UsedBridgeIds;
@@ -180,11 +180,15 @@ function parseSerializableRouteDescriptors(raw) {
180
180
  throw new Error('addon-routes: route descriptor missing method/path');
181
181
  }
182
182
  const description = Reflect.get(entry, 'description');
183
+ // An unknown limit name is dropped, never widened into "no limit" silently
184
+ // by a typo on the hub side: only names the hub implements are kept.
185
+ const rateLimit = Reflect.get(entry, 'rateLimit') === 'auth' ? 'auth' : undefined;
183
186
  return {
184
187
  method: asRouteMethod(method),
185
188
  path: routePath,
186
189
  access: asRouteAccess(Reflect.get(entry, 'access')),
187
190
  ...(typeof description === 'string' ? { description } : {}),
191
+ ...(rateLimit !== undefined ? { rateLimit } : {}),
188
192
  };
189
193
  });
190
194
  }
@@ -2686,6 +2690,7 @@ class AddonRegistryService {
2686
2690
  path: route.path,
2687
2691
  access: route.access,
2688
2692
  ...(route.description !== undefined ? { description: route.description } : {}),
2693
+ ...(route.rateLimit !== undefined ? { rateLimit: route.rateLimit } : {}),
2689
2694
  handler: async (req, reply) => {
2690
2695
  const envelope = await invoker.invoke({
2691
2696
  method: route.method,
@@ -448,6 +448,16 @@ class AgentRegistryService {
448
448
  this.moleculer.setOnAgentRegistered((agentId) => {
449
449
  void this.reconcileAgentAddons(agentId, 'node-registration');
450
450
  });
451
+ // D802: the mesh `transition` ends only once every agent this hub has
452
+ // EVER seen has authenticated — offline ones included. `null` (store not
453
+ // wired or not answering) never counts as "no agents".
454
+ this.moleculer.setKnownAgentsProvider(async () => {
455
+ const store = this.historyStore;
456
+ if (!store)
457
+ return null;
458
+ const rows = await store.tryGetAll();
459
+ return rows === null ? null : rows.filter((r) => !r.isHub).map((r) => r.id);
460
+ });
451
461
  this.broker.localBus.on('$node.connected', ({ node }) => {
452
462
  const kind = classifyNode(node.id);
453
463
  if (!kind)
@@ -1,7 +1,56 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AuthService = void 0;
3
+ exports.AuthService = exports.SessionRevokedError = void 0;
4
4
  const system_1 = require("@camstack/system");
5
+ /**
6
+ * A session-grade token whose server-side session is gone (logged out,
7
+ * revoked, expired) or cannot be checked right now. Every caller of
8
+ * `verifyToken` already treats a throw as "not authenticated".
9
+ */
10
+ class SessionRevokedError extends Error {
11
+ reason;
12
+ constructor(reason) {
13
+ super(`Session is no longer valid (${reason})`);
14
+ this.reason = reason;
15
+ this.name = 'SessionRevokedError';
16
+ }
17
+ }
18
+ exports.SessionRevokedError = SessionRevokedError;
19
+ /** A v2 session token: string `userId`, boolean `isAdmin`, no `kind` (not a bridge/challenge). */
20
+ function isSessionGrade(payload) {
21
+ return (Reflect.get(payload, 'kind') === undefined &&
22
+ typeof payload.userId === 'string' &&
23
+ typeof payload.isAdmin === 'boolean');
24
+ }
5
25
  class AuthService extends system_1.AuthManager {
26
+ sessionCheck = null;
27
+ /**
28
+ * Wire the login-session check (`hub-sessions.ts`). From then on EVERY
29
+ * `verifyToken` of a session-grade token — the tRPC boundary, addon routes,
30
+ * the session cookie, the OAuth authorize page, health, uploads — also
31
+ * requires its session to be live, so a revocation applies everywhere at
32
+ * once and no surface can forget to ask.
33
+ */
34
+ setSessionCheck(check) {
35
+ this.sessionCheck = check;
36
+ }
37
+ /**
38
+ * Why the session behind `payload` is no longer valid, or `null` when it is
39
+ * (or when no check is wired). For a principal resolved earlier — a
40
+ * WebSocket re-checks its session on every call without re-verifying an
41
+ * access token that may have expired since connect.
42
+ */
43
+ checkSession(payload) {
44
+ return this.sessionCheck === null ? null : this.sessionCheck(payload);
45
+ }
46
+ verifyToken(token) {
47
+ const payload = super.verifyToken(token);
48
+ if (this.sessionCheck !== null && isSessionGrade(payload)) {
49
+ const refusal = this.sessionCheck(payload);
50
+ if (refusal !== null)
51
+ throw new SessionRevokedError(refusal);
52
+ }
53
+ return payload;
54
+ }
6
55
  }
7
56
  exports.AuthService = AuthService;
@@ -79,14 +79,19 @@ class HandoffCodeService {
79
79
  /** Mint a single-use code bound to `userId`. Throws when the pending
80
80
  * store is full even after pruning expired entries (fail fast — a
81
81
  * legitimate flow never has anywhere near this many in flight). */
82
- create(userId) {
82
+ create(userId, options = {}) {
83
83
  this.prune();
84
84
  if (this.pending.size >= exports.HANDOFF_CODE_MAX_PENDING) {
85
85
  throw new Error('Too many pending handoff codes — try again shortly');
86
86
  }
87
87
  const code = `${exports.HANDOFF_CODE_PREFIX}${crypto.randomBytes(32).toString('hex')}`;
88
88
  const expiresAt = this.now() + this.ttlMs;
89
- this.pending.set(hashCode(code), { userId, expiresAt });
89
+ this.pending.set(hashCode(code), {
90
+ userId,
91
+ ...(options.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
92
+ purpose: options.purpose ?? 'renew',
93
+ expiresAt,
94
+ });
90
95
  return { code, expiresAt };
91
96
  }
92
97
  /** Redeem a raw code. Consumes the entry on the FIRST attempt no
@@ -99,7 +104,8 @@ class HandoffCodeService {
99
104
  this.pending.delete(hashCode(code));
100
105
  if (this.now() > entry.expiresAt)
101
106
  return null;
102
- return { userId: entry.userId };
107
+ const { expiresAt: _expiresAt, ...grant } = entry;
108
+ return grant;
103
109
  }
104
110
  /** Number of not-yet-redeemed (possibly expired) codes. Test/diag aid. */
105
111
  get pendingCount() {
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HubMeshGuard = void 0;
4
+ /**
5
+ * The hub's mesh guard (D802) — one object owning everything that decides
6
+ * whether another node may talk to the hub:
7
+ *
8
+ * - the {@link MeshAuthenticator} (who proved what, on which connection);
9
+ * - the broker middleware built on it (installed at broker construction);
10
+ * - the System → Cluster status slot;
11
+ * - the automatic end of `transition` ({@link decideMeshAutoEnforce}), checked
12
+ * after every successful join and once a minute.
13
+ *
14
+ * Kept out of `moleculer.service.ts` so that file only wires it.
15
+ */
16
+ const system_1 = require("@camstack/system");
17
+ const types_1 = require("@camstack/types");
18
+ /** How often `transition` is re-checked when nothing joins. */
19
+ const AUTO_ENFORCE_INTERVAL_MS = 60_000;
20
+ class HubMeshGuard {
21
+ ownNodeId;
22
+ logger;
23
+ authenticator;
24
+ startedAt = Date.now();
25
+ knownAgents = null;
26
+ listConnected = () => [];
27
+ timer = null;
28
+ evaluating = false;
29
+ lastWaitingFor = '';
30
+ constructor(ownNodeId, logger) {
31
+ this.ownNodeId = ownNodeId;
32
+ this.logger = logger;
33
+ this.authenticator = new system_1.MeshAuthenticator({
34
+ ownNodeId,
35
+ gate: system_1.getProcessClusterSecretGate,
36
+ mode: system_1.getProcessMeshAuthMode,
37
+ onAuthenticated: (nodeId) => {
38
+ this.logger.info('node authenticated to the mesh (challenge–response)', {
39
+ meta: { nodeId },
40
+ });
41
+ void this.evaluateAutoEnforce();
42
+ },
43
+ onTolerated: (nodeId, action) => {
44
+ this.logger.warn('node is calling the hub WITHOUT having authenticated — tolerated while System → ' +
45
+ 'Cluster is in Transition. Enter the cluster secret in this agent.', { meta: { nodeId, firstCall: action } });
46
+ },
47
+ });
48
+ (0, system_1.setProcessMeshAuthStatusSource)(() => this.authenticator.snapshot());
49
+ }
50
+ /** The middleware the hub broker must be constructed with. */
51
+ middleware() {
52
+ return (0, system_1.createMeshAccessMiddleware)({
53
+ ownNodeId: this.ownNodeId,
54
+ authenticator: this.authenticator,
55
+ logger: {
56
+ warn: (message, meta) => this.logger.warn(message, meta ? { meta } : undefined),
57
+ },
58
+ });
59
+ }
60
+ /** Start the periodic auto-enforce check; `listConnected` reads the live node list. */
61
+ start(listConnected) {
62
+ this.listConnected = listConnected;
63
+ if (this.timer !== null)
64
+ return;
65
+ this.timer = setInterval(() => void this.evaluateAutoEnforce(), AUTO_ENFORCE_INTERVAL_MS);
66
+ this.timer.unref?.();
67
+ }
68
+ /** Wired by `AgentRegistryService`, which owns the cluster-node history. */
69
+ setKnownAgentsProvider(provider) {
70
+ this.knownAgents = provider;
71
+ }
72
+ /** A node's connection dropped. */
73
+ forget(nodeId) {
74
+ this.authenticator.forget(nodeId);
75
+ }
76
+ stop() {
77
+ if (this.timer !== null)
78
+ clearInterval(this.timer);
79
+ this.timer = null;
80
+ (0, system_1.setProcessMeshAuthStatusSource)(null);
81
+ }
82
+ /** Public for the spec; the timer and every join call it. */
83
+ async evaluateAutoEnforce() {
84
+ if (this.evaluating || (0, system_1.getProcessMeshAuthMode)() !== 'transition')
85
+ return;
86
+ this.evaluating = true;
87
+ try {
88
+ let knownAgents = null;
89
+ if (this.knownAgents !== null) {
90
+ try {
91
+ knownAgents = await this.knownAgents();
92
+ }
93
+ catch {
94
+ knownAgents = null;
95
+ }
96
+ }
97
+ const decision = (0, system_1.decideMeshAutoEnforce)({
98
+ mode: (0, system_1.getProcessMeshAuthMode)(),
99
+ gate: (0, system_1.getProcessClusterSecretGate)().kind,
100
+ uptimeMs: Date.now() - this.startedAt,
101
+ knownAgents,
102
+ connected: this.listConnected(),
103
+ toleratedEver: this.authenticator.everToleratedNodeIds(),
104
+ authenticatedEver: this.authenticator.everAuthenticatedNodeIds(),
105
+ ownNodeId: this.ownNodeId,
106
+ });
107
+ if (!decision.enforce) {
108
+ const waiting = decision.waitingFor.join(',');
109
+ if (waiting.length > 0 && waiting !== this.lastWaitingFor) {
110
+ this.logger.info('mesh transition: still waiting for agents to authenticate', {
111
+ meta: { waitingFor: decision.waitingFor },
112
+ });
113
+ }
114
+ this.lastWaitingFor = waiting;
115
+ return;
116
+ }
117
+ const done = await (0, system_1.requestProcessMeshEnforcement)(decision.reason);
118
+ if (done) {
119
+ this.logger.info('mesh authentication switched to ENFORCED', {
120
+ meta: { reason: decision.reason },
121
+ });
122
+ }
123
+ }
124
+ catch (err) {
125
+ this.logger.warn('mesh auto-enforce check failed', { meta: { error: (0, types_1.errMsg)(err) } });
126
+ }
127
+ finally {
128
+ this.evaluating = false;
129
+ }
130
+ }
131
+ }
132
+ exports.HubMeshGuard = HubMeshGuard;
@@ -9,6 +9,7 @@ const types_1 = require("@camstack/types");
9
9
  const cap_router_runtime_js_1 = require("../../api/trpc/cap-router-runtime.js");
10
10
  const core_cap_bridge_js_1 = require("../../api/trpc/core-cap-bridge.js");
11
11
  const agent_readiness_pull_js_1 = require("./agent-readiness-pull.js");
12
+ const hub_mesh_guard_js_1 = require("./hub-mesh-guard.js");
12
13
  const cap_call_fn_js_1 = require("./cap-call-fn.js");
13
14
  const cap_route_authority_js_1 = require("./cap-route-authority.js");
14
15
  class MoleculerService {
@@ -23,6 +24,12 @@ class MoleculerService {
23
24
  return this.broker;
24
25
  }
25
26
  logger;
27
+ /**
28
+ * D802 — who may talk to the hub over the mesh: the challenge–response join,
29
+ * the broker middleware that refuses / tolerates unauthenticated nodes and
30
+ * strips forged identities, and the automatic end of `transition`.
31
+ */
32
+ meshGuard;
26
33
  /**
27
34
  * D3 authority: union of every node's manifest delivered via
28
35
  * `$hub.registerNode`. Populated by `onRegisterNode` in `hubDeps`.
@@ -190,9 +197,12 @@ class MoleculerService {
190
197
  // can answer, and the Moleculer namespace was never authentication (TCP
191
198
  // ignores it; UDP discovery broadcasts it in clear). The secret gates
192
199
  // `$hub.registerNode`, read from the keychain at call time.
200
+ this.meshGuard = new hub_mesh_guard_js_1.HubMeshGuard('hub', this.logging.createLogger('mesh-auth'));
193
201
  const broker = (0, system_1.createBroker)({
194
202
  nodeID: 'hub',
195
203
  mode: 'hub',
204
+ // D802: every action and event another node sends passes the mesh gate.
205
+ middlewares: [this.meshGuard.middleware()],
196
206
  logLevel: this.config.get('moleculer.logLevel') ?? 'warn',
197
207
  ...(tcpPort && !Number.isNaN(tcpPort) ? { tcpPort } : {}),
198
208
  ...(udpPort && !Number.isNaN(udpPort) ? { udpPort } : {}),
@@ -217,6 +227,23 @@ class MoleculerService {
217
227
  setOnAgentRegistered(cb) {
218
228
  this.onAgentRegisteredCb = cb;
219
229
  }
230
+ /**
231
+ * D802: the cluster-node history, for the automatic end of the mesh
232
+ * `transition` — wired by `AgentRegistryService`, which owns the history.
233
+ */
234
+ setKnownAgentsProvider(provider) {
235
+ this.meshGuard.setKnownAgentsProvider(provider);
236
+ }
237
+ /** Every node Moleculer currently lists as available (this hub included). */
238
+ listAvailableNodeIds() {
239
+ const registry = this.broker.registry;
240
+ try {
241
+ return (registry?.getNodeList?.({ onlyAvailable: true }) ?? []).map((n) => n.id);
242
+ }
243
+ catch {
244
+ return [];
245
+ }
246
+ }
220
247
  async onModuleInit() {
221
248
  const logger = this.logging.createLogger('moleculer');
222
249
  const hubDeps = {
@@ -270,6 +297,8 @@ class MoleculerService {
270
297
  },
271
298
  // System → Cluster, from the keychain (D797): off / on / unknown.
272
299
  clusterSecretGate: system_1.getProcessClusterSecretGate,
300
+ // D802: challenge–response join + who may register.
301
+ meshAuth: this.meshGuard.authenticator,
273
302
  };
274
303
  const hubService = (0, system_1.createHubService)(hubDeps);
275
304
  this.brokerSafe.createService(hubService);
@@ -613,6 +642,8 @@ class MoleculerService {
613
642
  // the incarnation) and this call is refused if the id has been reclaimed;
614
643
  // remote nodes register no owner at all, so they clean up as before.
615
644
  this.removeNodeFromRegistry(node.id);
645
+ // D802: a node authenticates again on its next connection.
646
+ this.meshGuard.forget(node.id);
616
647
  });
617
648
  // D8 reverse-leg snapshot-reconcile: on every agent (re)connect, pull
618
649
  // the agent's own readiness snapshot (`$agent-readiness.getSnapshot`)
@@ -661,6 +692,7 @@ class MoleculerService {
661
692
  }));
662
693
  await this.brokerSafe.start();
663
694
  logger.info('Moleculer broker started (TCP transport)');
695
+ this.meshGuard.start(() => this.listAvailableNodeIds());
664
696
  // Construct the CapRouteResolver now that both the broker and
665
697
  // localChildRegistry are ready. The resolver reads live registry state
666
698
  // via closure accessors on every call (not a frozen snapshot), so new
@@ -1290,6 +1322,7 @@ class MoleculerService {
1290
1322
  }
1291
1323
  }
1292
1324
  async onModuleDestroy() {
1325
+ this.meshGuard.stop();
1293
1326
  if (this.agentReadinessPullTimer !== null) {
1294
1327
  clearInterval(this.agentReadinessPullTimer);
1295
1328
  this.agentReadinessPullTimer = null;
package/dist/main.js CHANGED
@@ -86,12 +86,17 @@ const ws_request_census_1 = require("./api/trpc/ws-request-census");
86
86
  const ws_slow_consumer_guard_js_1 = require("./api/trpc/ws-slow-consumer-guard.js");
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
+ const auth_network_policy_js_1 = require("./auth/auth-network-policy.js");
90
+ const hub_sessions_js_1 = require("./auth/hub-sessions.js");
91
+ const session_lifetimes_js_1 = require("./auth/session-lifetimes.js");
89
92
  const auth_rate_limit_1 = require("./auth/auth-rate-limit");
90
93
  const external_link_intents_js_1 = require("./auth/external-link-intents.js");
91
94
  const session_cookie_js_1 = require("./auth/session-cookie.js");
92
95
  const session_mint_js_1 = require("./auth/session-mint.js");
93
- const sso_finish_js_1 = require("./auth/sso-finish.js");
94
96
  const sso_finish_deps_js_1 = require("./auth/sso-finish-deps.js");
97
+ const sso_routes_js_1 = require("./auth/sso-routes.js");
98
+ const used_bridge_ids_js_1 = require("./auth/used-bridge-ids.js");
99
+ const native_link_completions_js_1 = require("./auth/native-link-completions.js");
95
100
  const boot_config_1 = require("./boot/boot-config");
96
101
  const platform_keychain_key_1 = require("./boot/platform-keychain-key");
97
102
  const integration_id_backfill_1 = require("./boot/integration-id-backfill");
@@ -410,6 +415,16 @@ async function bootstrap() {
410
415
  storageService.setLocationManager(infra.locationManager);
411
416
  // ConfigService — SettingsStore is wired later by the settings-store capability consumer
412
417
  const config = app.get(config_service_1.ConfigService);
418
+ // The real client address behind a trusted proxy, and the opt-in trusted
419
+ // networks — read from settings per request (auth-network-policy.ts).
420
+ const authNetwork = (0, auth_network_policy_js_1.createAuthNetworkPolicy)((key) => config.get(key));
421
+ // Public addon routes that declare `rateLimit: 'auth'` (an IdP start /
422
+ // callback) and the SSO finish: 30 per minute per real client address.
423
+ const authRouteLimiter = (0, auth_rate_limit_1.createRateLimiter)({ windowMs: 60_000, max: 30 });
424
+ // Login sessions (local-auth's table, reached through its hub-internal
425
+ // surface): every session token minted from here on carries a `sid`, and
426
+ // every verify of one asks whether that session is still live.
427
+ const sessionLifetimes = () => (0, session_lifetimes_js_1.readSessionLifetimes)((key) => config.get(key));
413
428
  // Register addon upload route (multipart — must be registered before tRPC).
414
429
  // We pass AddonRegistryService so the handler can resolve the
415
430
  // `user-management` cap singleton at request time (the only working
@@ -498,6 +513,11 @@ async function bootstrap() {
498
513
  addonRegistry.setDataPlaneRegistry(dataPlaneRegistry);
499
514
  // ── Configure the CapabilityRegistry (created in AddonRegistryService constructor) ──
500
515
  const capabilityRegistry = addonRegistry.getCapabilityRegistry();
516
+ const sessionBacking = (0, hub_sessions_js_1.sessionBackingFrom)(capabilityRegistry, sessionLifetimes);
517
+ // From here on every verify of a session-grade token — tRPC, addon routes,
518
+ // the session cookie, OAuth authorize, uploads, health — requires its
519
+ // session to be live (`hub-sessions.ts`).
520
+ app.get(auth_service_1.AuthService).setSessionCheck((0, hub_sessions_js_1.createSessionTokenCheck)(capabilityRegistry));
501
521
  // Declare every shipped capability BEFORE app.init() so addon
502
522
  // registerProvider() calls (in-process or via the Moleculer bridge
503
523
  // from forked workers / agents) find their definition during
@@ -507,7 +527,7 @@ async function bootstrap() {
507
527
  capabilityRegistry.declareCapability(capDef);
508
528
  }
509
529
  // Hub-internal `sso-bridge` provider — gives auth-provider addons
510
- // (OIDC, SAML, magic-link, …) a typed gateway to mint an HMAC-signed
530
+ // (OIDC, SAML, …) a typed gateway to mint an HMAC-signed
511
531
  // bridge token before redirecting to `/api/auth/sso/finish`. Without
512
532
  // this, the finish endpoint would have to trust unsigned query params
513
533
  // (anyone could craft `?isAdmin=1`). Backed by AuthService.
@@ -538,12 +558,41 @@ async function bootstrap() {
538
558
  // Pending "link this sign-in account" intents (D782) — minted by
539
559
  // `auth.beginExternalLink`, consumed once by `/api/auth/sso/finish`.
540
560
  const externalLinkIntents = new external_link_intents_js_1.ExternalLinkIntents();
561
+ // Native-app links parked for the app to claim, and one-time codes that
562
+ // open the account page signed in, in a browser (D801).
563
+ const nativeLinkCompletions = new native_link_completions_js_1.NativeLinkCompletions();
564
+ // `jti`s of login bridges already finished — a bridge is single-use.
565
+ const usedBridgeIds = new used_bridge_ids_js_1.UsedBridgeIds();
566
+ const browserHandoffCodes = new handoff_code_service_1.HandoffCodeService();
567
+ const nativeLinkMinter = (0, session_mint_js_1.createSessionMinter)({
568
+ signer: app.get(auth_service_1.AuthService),
569
+ directory: () => capabilityRegistry.getSingleton('user-management'),
570
+ sessions: sessionBacking,
571
+ });
572
+ const nativeLinkWiring = () => ({
573
+ auth: app.get(auth_service_1.AuthService),
574
+ registry: capabilityRegistry,
575
+ moleculer: app.get(moleculer_service_1.MoleculerService),
576
+ minter: nativeLinkMinter,
577
+ linkIntents: externalLinkIntents,
578
+ nativeLinks: nativeLinkCompletions,
579
+ usedBridgeIds,
580
+ });
541
581
  const buildRouter = () => (0, trpc_router_1.buildAppRouter)({
542
582
  authService: app.get(auth_service_1.AuthService),
543
583
  shareTokenService,
544
584
  handoffCodeService,
545
585
  externalLinkIntents,
586
+ nativeLink: {
587
+ completions: nativeLinkCompletions,
588
+ browserHandoffs: browserHandoffCodes,
589
+ // The SSO finish's own link step — the same conflict rule, the same
590
+ // store — run for the app's claim instead of the browser's return.
591
+ link: (input) => (0, sso_finish_deps_js_1.createSsoFinishDeps)(nativeLinkWiring(), undefined).identities.link(input),
592
+ },
546
593
  configService: config,
594
+ authNetworkPolicy: authNetwork,
595
+ sessionBacking,
547
596
  featureService: app.get(feature_service_1.FeatureService),
548
597
  loggingService,
549
598
  eventBus: app.get(event_bus_service_1.EventBusService),
@@ -582,7 +631,11 @@ async function bootstrap() {
582
631
  beforeAddonBoot: () => {
583
632
  try {
584
633
  appRouter = buildRouter();
585
- app.get(moleculer_service_1.MoleculerService).registerCoreCapService((0, core_cap_bridge_1.buildCoreCapService)(appRouter));
634
+ {
635
+ const meshBroker = app.get(moleculer_service_1.MoleculerService);
636
+ // D802: another node's call runs as `node:<id>`, never the hub's own principal.
637
+ meshBroker.registerCoreCapService((0, core_cap_bridge_1.buildCoreCapService)(appRouter, undefined, { ownNodeId: meshBroker.nodeId }));
638
+ }
586
639
  console.log('[bootstrap] core-cap mesh bridge registered ($core-caps) — before addon boot');
587
640
  }
588
641
  catch (err) {
@@ -629,6 +682,7 @@ async function bootstrap() {
629
682
  (0, adaptive_probe_routes_1.registerAdaptiveProbeRoute)(fastify, {
630
683
  verifyToken: (token) => app.get(auth_service_1.AuthService).verifyToken(token),
631
684
  limiter: (0, auth_rate_limit_1.createRateLimiter)({ windowMs: 60_000, max: adaptive_probe_routes_1.ADAPTIVE_PROBE_PER_IP_PER_MINUTE }),
685
+ network: authNetwork,
632
686
  });
633
687
  }
634
688
  catch (err) {
@@ -670,7 +724,11 @@ async function bootstrap() {
670
724
  if (appRouter === null) {
671
725
  appRouter = buildRouter();
672
726
  try {
673
- app.get(moleculer_service_1.MoleculerService).registerCoreCapService((0, core_cap_bridge_1.buildCoreCapService)(appRouter));
727
+ {
728
+ const meshBroker = app.get(moleculer_service_1.MoleculerService);
729
+ // D802: another node's call runs as `node:<id>`, never the hub's own principal.
730
+ meshBroker.registerCoreCapService((0, core_cap_bridge_1.buildCoreCapService)(appRouter, undefined, { ownNodeId: meshBroker.nodeId }));
731
+ }
674
732
  console.log('[bootstrap] core-cap mesh bridge registered ($core-caps) — late fallback');
675
733
  }
676
734
  catch (err) {
@@ -820,37 +878,31 @@ async function bootstrap() {
820
878
  reply.header('cache-control', 'public, max-age=86400');
821
879
  return reply.send(fs.readFileSync(filePath));
822
880
  });
823
- // ── SSO finish endpoint ─────────────────────────────────────────
824
- // OIDC / magic-link redirect here with an HMAC-signed LOGIN bridge
825
- // (`sso-bridge` cap). The bridge says who signed in; the session is
826
- // minted by THE session mint from the user record as it is now (D712) —
827
- // `auth/sso-finish.ts` holds the rules, this route only maps the reply.
828
- const ssoAuth = app.get(auth_service_1.AuthService);
829
- const ssoLogger = app.get(logging_service_1.LoggingService).createLogger('sso-finish');
881
+ // ── SSO finish + browser handoff ────────────────────────────────
882
+ // OIDC redirects to the finish with an HMAC-signed, single-use LOGIN
883
+ // bridge bound to the browser that started it; the session is minted by
884
+ // THE session mint from the user record as it is now (D712). Rules in
885
+ // `auth/sso-finish.ts`, routes in `auth/sso-routes.ts`.
830
886
  const ssoMinter = (0, session_mint_js_1.createSessionMinter)({
831
- signer: ssoAuth,
887
+ signer: app.get(auth_service_1.AuthService),
832
888
  directory: () => capabilityRegistry.getSingleton('user-management'),
889
+ sessions: sessionBacking,
833
890
  });
834
- fastify.get('/api/auth/sso/finish', async (request, reply) => {
835
- const outcome = await (0, sso_finish_js_1.finishSsoLogin)(request.query.bridge, (0, sso_finish_deps_js_1.createSsoFinishDeps)({
836
- auth: ssoAuth,
891
+ (0, sso_routes_js_1.registerSsoRoutes)(fastify, {
892
+ wiring: {
893
+ auth: app.get(auth_service_1.AuthService),
837
894
  registry: capabilityRegistry,
838
895
  moleculer: app.get(moleculer_service_1.MoleculerService),
839
896
  minter: ssoMinter,
840
897
  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' });
847
- if (outcome.kind === 'error')
848
- return reply.status(outcome.status).send(outcome.body);
849
- reply.code(302);
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}`);
853
- return reply.send('');
898
+ nativeLinks: nativeLinkCompletions,
899
+ usedBridgeIds,
900
+ },
901
+ network: authNetwork,
902
+ limiter: authRouteLimiter,
903
+ browserHandoffCodes,
904
+ minter: ssoMinter,
905
+ logger: app.get(logging_service_1.LoggingService).createLogger('sso-finish'),
854
906
  });
855
907
  // ── Backup archive download ─────────────────────────────────────
856
908
  // Streams an archive through the storage cap's chunked-download
@@ -1052,6 +1104,18 @@ async function bootstrap() {
1052
1104
  }
1053
1105
  return reply.status(404).send({ error: 'Not found' });
1054
1106
  }
1107
+ // A route that asked for it (an IdP start / callback) is limited per
1108
+ // REAL client address before its handler runs.
1109
+ if (match.route.rateLimit === 'auth') {
1110
+ const routeClient = (0, auth_rate_limit_1.clientKeyFromRequest)(request, authNetwork);
1111
+ if (routeClient !== null) {
1112
+ const verdict = authRouteLimiter.check(`route:${addonId}:${routeClient}`);
1113
+ if (!verdict.allowed) {
1114
+ reply.header('retry-after', String(verdict.retryAfterSeconds));
1115
+ return reply.status(429).send({ error: 'Too many attempts' });
1116
+ }
1117
+ }
1118
+ }
1055
1119
  // Auth check based on route.access
1056
1120
  if (match.route.access !== 'public') {
1057
1121
  const authHeader = request.headers.authorization;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.486",
3
+ "version": "1.2.487",
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.359",
41
- "@camstack/addon-agent-ui": "1.2.195",
42
- "@camstack/addon-auth": "1.2.134",
43
- "@camstack/addon-decoder-nodeav": "1.2.121",
44
- "@camstack/addon-notifiers": "1.2.128",
45
- "@camstack/addon-pipeline": "1.2.346",
46
- "@camstack/addon-pipeline-orchestrator": "1.2.298",
47
- "@camstack/addon-post-analysis": "1.2.347",
48
- "@camstack/sdk": "1.2.127",
49
- "@camstack/system": "1.2.381",
50
- "@camstack/types": "1.2.332",
51
- "@camstack/ui-library": "1.2.253",
40
+ "@camstack/addon-admin-ui": "1.2.360",
41
+ "@camstack/addon-agent-ui": "1.2.196",
42
+ "@camstack/addon-auth": "1.2.135",
43
+ "@camstack/addon-decoder-nodeav": "1.2.122",
44
+ "@camstack/addon-notifiers": "1.2.129",
45
+ "@camstack/addon-pipeline": "1.2.347",
46
+ "@camstack/addon-pipeline-orchestrator": "1.2.299",
47
+ "@camstack/addon-post-analysis": "1.2.348",
48
+ "@camstack/sdk": "1.2.128",
49
+ "@camstack/system": "1.2.382",
50
+ "@camstack/types": "1.2.333",
51
+ "@camstack/ui-library": "1.2.254",
52
52
  "@fastify/compress": "^9.0.0",
53
53
  "@fastify/cookie": "^11.0.2",
54
54
  "@fastify/cors": "^11.2.0",