@camstack/server 1.2.486 → 1.2.488

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 (38) hide show
  1. package/dist/agent/agent-mesh-join.js +121 -0
  2. package/dist/agent/agent-service.js +4 -0
  3. package/dist/agent/main.js +62 -2
  4. package/dist/api/auth-whoami.js +1 -1
  5. package/dist/api/core/auth-admin-tokens.procedures.js +138 -0
  6. package/dist/api/core/auth-limits.js +77 -0
  7. package/dist/api/core/auth-reauth.procedures.js +108 -0
  8. package/dist/api/core/auth-sessions.procedures.js +166 -0
  9. package/dist/api/core/auth.router.js +281 -80
  10. package/dist/api/health/adaptive-probe.routes.js +1 -1
  11. package/dist/api/mesh/enroll.routes.js +28 -0
  12. package/dist/api/trpc/core-cap-bridge.js +88 -5
  13. package/dist/api/trpc/generated-cap-mounts.js +19 -1
  14. package/dist/api/trpc/generated-cap-routers.js +512 -205
  15. package/dist/api/trpc/trpc.context.js +86 -34
  16. package/dist/api/trpc/trpc.middleware.js +8 -11
  17. package/dist/api/trpc/trpc.router.js +6 -1
  18. package/dist/auth/auth-network-policy.js +75 -0
  19. package/dist/auth/auth-rate-limit.js +25 -20
  20. package/dist/auth/client-address.js +68 -0
  21. package/dist/auth/external-link-intents.js +12 -3
  22. package/dist/auth/hub-sessions.js +74 -0
  23. package/dist/auth/native-link-completions.js +68 -0
  24. package/dist/auth/native-return.js +33 -0
  25. package/dist/auth/session-lifetimes.js +46 -0
  26. package/dist/auth/session-mint.js +110 -13
  27. package/dist/auth/sso-finish-deps.js +56 -1
  28. package/dist/auth/sso-finish.js +132 -17
  29. package/dist/auth/sso-routes.js +95 -0
  30. package/dist/auth/used-bridge-ids.js +52 -0
  31. package/dist/core/addon/addon-registry.service.js +5 -0
  32. package/dist/core/agent/agent-registry.service.js +23 -0
  33. package/dist/core/auth/auth.service.js +50 -1
  34. package/dist/core/auth/handoff-code.service.js +9 -3
  35. package/dist/core/moleculer/hub-mesh-guard.js +132 -0
  36. package/dist/core/moleculer/moleculer.service.js +46 -0
  37. package/dist/main.js +97 -29
  38. package/package.json +13 -13
@@ -53,7 +53,7 @@ function registerAdaptiveProbeRoute(fastify, deps) {
53
53
  }, deps.verifyToken);
54
54
  if (!authorized)
55
55
  return reply.status(401).send({ ok: false, error: 'unauthorized' });
56
- const key = (0, auth_rate_limit_js_1.clientKeyFromRequest)(req);
56
+ const key = (0, auth_rate_limit_js_1.clientKeyFromRequest)(req, deps.network);
57
57
  if (key !== null) {
58
58
  const verdict = deps.limiter.check(`adaptive-probe:${key}`);
59
59
  if (!verdict.allowed) {
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerMeshEnrollRoute = registerMeshEnrollRoute;
4
+ const system_1 = require("@camstack/system");
5
+ function registerMeshEnrollRoute(fastify) {
6
+ fastify.post('/api/mesh/enroll', async (request, reply) => {
7
+ const caState = (0, system_1.getProcessMeshCaState)();
8
+ if (caState.state !== 'ready') {
9
+ return reply.status(503).send({
10
+ ok: false,
11
+ error: `mesh CA unavailable: ${caState.reason}`,
12
+ });
13
+ }
14
+ const body = request.body ?? {};
15
+ const result = await (0, system_1.enrollMeshNode)({ token: body.token ?? '', csr: body.csr ?? '' }, { ca: caState.ca });
16
+ if (!result.ok) {
17
+ return reply.status(result.status).send({ ok: false, error: result.error });
18
+ }
19
+ return reply.send({
20
+ ok: true,
21
+ cert: result.cert,
22
+ caCert: result.caCert,
23
+ nodeId: result.nodeId,
24
+ notAfter: result.notAfter,
25
+ serialNumber: result.serialNumber,
26
+ });
27
+ });
28
+ }
@@ -18,10 +18,15 @@
18
18
  * routes the call exactly like any addon-provided cap.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.REMOTE_PRIVILEGE_LOG_ONLY = exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = exports.REMOTE_DENIED_NAMESPACES = void 0;
22
+ exports.setRemotePrivilegeLogSink = setRemotePrivilegeLogSink;
23
+ exports.__resetRemotePrivilegeObservationForTests = __resetRemotePrivilegeObservationForTests;
24
+ exports.observeRemotePrivilege = observeRemotePrivilege;
21
25
  exports.isCoreServiceCapName = isCoreServiceCapName;
22
26
  exports.describeMeshOrigin = describeMeshOrigin;
23
27
  exports.buildCoreCapService = buildCoreCapService;
24
28
  const system_1 = require("@camstack/system");
29
+ const moleculer_1 = require("moleculer");
25
30
  const http_request_census_singleton_js_1 = require("./http-request-census.singleton.js");
26
31
  const server_provided_caps_js_1 = require("./server-provided-caps.js");
27
32
  const trpc_context_js_1 = require("./trpc.context.js");
@@ -37,6 +42,10 @@ const trpc_middleware_js_1 = require("./trpc.middleware.js");
37
42
  * - `auth` — issuing service / scoped tokens over the trusted mesh
38
43
  * would let any forked addon mint admin credentials. Addons must
39
44
  * not perform authentication operations; this stays hub-local.
45
+ * - `repl` — `repl.execute` runs code in the hub process (`vm` is no
46
+ * boundary). The admin UI's REPL reaches it over HTTP tRPC as an admin
47
+ * session; nothing calls it over the mesh, and a mesh caller must never
48
+ * be able to (D802, security plan §1).
40
49
  * - `live`, `systemEvents` — listed in `NEVER_BRIDGED_CAPS`
41
50
  * (`trpc-links.ts`). They are hub-only push streams, not
42
51
  * request/reply, and the worker side fast-fails on them by design.
@@ -51,9 +60,57 @@ const CORE_NAMESPACES = new Set([
51
60
  'hwaccel',
52
61
  'streamProbe',
53
62
  'eventBusProxy',
54
- 'repl',
55
63
  'addonSettingsRaw',
56
64
  ]);
65
+ /**
66
+ * Bridged namespaces another NODE may not call (D802) — only the hub's own
67
+ * runners may. `logs` reads the hub's whole log store, `addonSettingsRaw` every
68
+ * addon's raw config (tokens included). No addon calls either over the mesh
69
+ * (they read their own settings through the settings door), so refusing them
70
+ * to an agent costs nothing and takes them off the LAN.
71
+ */
72
+ exports.REMOTE_DENIED_NAMESPACES = new Set([
73
+ 'logs',
74
+ 'addonSettingsRaw',
75
+ ]);
76
+ /** Moleculer error `type` of a bridged call another node may not make. */
77
+ exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE = 'CORE_CAP_REMOTE_FORBIDDEN';
78
+ /**
79
+ * When true, a remote node's allowed bridged calls are logged once per
80
+ * `(nodeId, path)` per process — still admin, not refused. Granting least
81
+ * privilege waits on census; this is the log-only week before that.
82
+ */
83
+ exports.REMOTE_PRIVILEGE_LOG_ONLY = true;
84
+ let remotePrivilegeLogSink = null;
85
+ /** Attach the warn sink used by {@link observeRemotePrivilege}. */
86
+ function setRemotePrivilegeLogSink(sink) {
87
+ remotePrivilegeLogSink = sink;
88
+ }
89
+ const remotePrivilegeObserved = new Set();
90
+ /** Clears dedupe state and the sink — tests only. */
91
+ function __resetRemotePrivilegeObservationForTests() {
92
+ remotePrivilegeObserved.clear();
93
+ remotePrivilegeLogSink = null;
94
+ }
95
+ function remotePrivilegeKey(nodeId, path) {
96
+ return `${nodeId}\0${path}`;
97
+ }
98
+ /**
99
+ * One warn per `(nodeId, path)` while {@link REMOTE_PRIVILEGE_LOG_ONLY} is on.
100
+ * Call only after {@link REMOTE_DENIED_NAMESPACES} has passed.
101
+ */
102
+ function observeRemotePrivilege(nodeId, path) {
103
+ if (!exports.REMOTE_PRIVILEGE_LOG_ONLY)
104
+ return;
105
+ const key = remotePrivilegeKey(nodeId, path);
106
+ if (remotePrivilegeObserved.has(key))
107
+ return;
108
+ remotePrivilegeObserved.add(key);
109
+ const message = `mesh privilege: node:${nodeId} called ${path} (log-only; still allowed)`;
110
+ remotePrivilegeLogSink?.warn(message, {
111
+ meta: { nodeId, path, principal: `node:${nodeId}` },
112
+ });
113
+ }
57
114
  /**
58
115
  * camelCase → kebab-case. Mirrors `toKebab` in `trpc-links.ts` so the
59
116
  * action name matches what `brokerTransportLink` derives from `op.path`.
@@ -164,9 +221,21 @@ function resolveInvoker(caller, path) {
164
221
  * appRouter. Query + mutation procedures in {@link CORE_NAMESPACES} are
165
222
  * exposed; subscriptions are skipped (the broker transport is
166
223
  * request/reply only).
224
+ *
225
+ * Each call runs under a principal naming its origin (D802): `mesh` for the
226
+ * hub's own runners, `node:<nodeId>` for another node.
167
227
  */
168
- function buildCoreCapService(appRouter, census = (0, http_request_census_singleton_js_1.getHttpRequestCensus)()) {
169
- const meshCaller = (0, trpc_middleware_js_1.createCallerFactory)(appRouter)((0, trpc_context_js_1.createMeshTrpcContext)());
228
+ function buildCoreCapService(appRouter, census = (0, http_request_census_singleton_js_1.getHttpRequestCensus)(), options = {}) {
229
+ const callerFactory = (0, trpc_middleware_js_1.createCallerFactory)(appRouter);
230
+ const callers = new Map();
231
+ const callerFor = (principal) => {
232
+ let caller = callers.get(principal);
233
+ if (caller === undefined) {
234
+ caller = callerFactory((0, trpc_context_js_1.createMeshTrpcContext)(principal));
235
+ callers.set(principal, caller);
236
+ }
237
+ return caller;
238
+ };
170
239
  const discovered = [];
171
240
  collectProcedures(appRouter._def.procedures, '', discovered);
172
241
  const actions = [];
@@ -179,17 +248,31 @@ function buildCoreCapService(appRouter, census = (0, http_request_census_singlet
179
248
  continue;
180
249
  if (type === 'subscription')
181
250
  continue;
182
- const invoke = resolveInvoker(meshCaller, path);
183
- if (invoke === null)
251
+ if (resolveInvoker(callerFor('mesh'), path) === null)
184
252
  continue;
185
253
  const method = path.slice(dot + 1);
186
254
  actions.push({
187
255
  actionName: `${toKebab(namespace)}.${method}`,
188
256
  invoke: (input, origin) => {
189
257
  countMeshCall(census, path, origin);
258
+ const remoteNode = remoteOrigin(origin, options.ownNodeId);
259
+ if (remoteNode !== null && exports.REMOTE_DENIED_NAMESPACES.has(namespace)) {
260
+ throw new moleculer_1.Errors.MoleculerClientError(`"${path}" is not callable from another node (caller "${remoteNode}")`, 403, exports.CORE_CAP_REMOTE_FORBIDDEN_TYPE);
261
+ }
262
+ if (remoteNode !== null)
263
+ observeRemotePrivilege(remoteNode, path);
264
+ const invoke = resolveInvoker(callerFor(remoteNode === null ? 'mesh' : `node:${remoteNode}`), path);
265
+ if (invoke === null)
266
+ throw new Error(`$core-caps: "${path}" is not invocable`);
190
267
  return invoke(input);
191
268
  },
192
269
  });
193
270
  }
194
271
  return (0, system_1.createCoreCapService)({ actions });
195
272
  }
273
+ /** The calling node when it is not the hub itself; `null` for the hub's own calls. */
274
+ function remoteOrigin(origin, ownNodeId) {
275
+ if (ownNodeId === undefined || origin.nodeId === null)
276
+ return null;
277
+ return origin.nodeId === ownNodeId ? null : origin.nodeId;
278
+ }
@@ -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: 164 Skipped (legacy): 3
5
+ // Mounted: 166 Skipped (legacy): 3
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.LEGACY_SHAPE_SKIP = void 0;
8
8
  exports.mountAllCaps = mountAllCaps;
@@ -530,6 +530,24 @@ function mountAllCaps(services) {
530
530
  terminalSession: (0, generated_cap_routers_js_1.createCapRouter_terminalSession)((_ctx) => reg?.getSingleton('terminal-session') ??
531
531
  null, remoteCapProxy),
532
532
  toast: (0, generated_cap_routers_js_1.createCapRouter_toast)((_ctx) => reg?.getSingleton('toast') ?? null, remoteCapProxy),
533
+ training: (0, generated_cap_routers_js_1.createCapRouter_training)((_ctx) => reg?.getSingleton('training') ?? null, remoteCapProxy),
534
+ trainingProvider: (0, generated_cap_routers_js_1.createCapRouter_trainingProvider)((_ctx, addonId) => {
535
+ if (!reg)
536
+ return null;
537
+ if (addonId !== undefined) {
538
+ return reg.getProviderByAddonId('training-provider', addonId);
539
+ }
540
+ const entries = reg.getCollectionEntries('training-provider');
541
+ if (entries.length === 0)
542
+ return null;
543
+ const providers = entries.map(([, p]) => p);
544
+ const first = providers[0];
545
+ return {
546
+ ...first,
547
+ describe: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'describe'),
548
+ listJobs: (0, cap_mount_helpers_js_1.concatCollection)(providers, 'listJobs'),
549
+ };
550
+ }, remoteCapProxy),
533
551
  turnProvider: (0, generated_cap_routers_js_1.createCapRouter_turnProvider)((_ctx, addonId) => {
534
552
  if (!reg)
535
553
  return null;