@camstack/server 1.2.563 → 1.2.565
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.
- package/dist/agent/agent-http.js +23 -2
- package/dist/agent/agent-mesh-enroll.js +2 -0
- package/dist/agent/agent-node-certificates.js +294 -0
- package/dist/agent/agent-reconnect.js +34 -0
- package/dist/agent/agent-sniffing-listener.js +263 -0
- package/dist/agent/agent-update-service.js +4 -1
- package/dist/agent/main.js +101 -37
- package/dist/agent/serial-queue.js +49 -0
- package/dist/api/core/auth-device-cert.procedures.js +435 -0
- package/dist/api/core/auth-sessions.procedures.js +20 -2
- package/dist/api/core/auth.router.js +190 -7
- package/dist/api/core/certificates.router.js +505 -0
- package/dist/api/mesh/enroll.routes.js +7 -1
- package/dist/api/trpc/generated-cap-mounts.js +4 -1
- package/dist/api/trpc/generated-cap-routers.js +2006 -1859
- package/dist/api/trpc/trpc.router.js +45 -0
- package/dist/auth/device-enroll-proofs.js +148 -0
- package/dist/auth/hub-sessions.js +2 -0
- package/dist/auth/session-mint.js +24 -2
- package/dist/auth/sso-routes.js +1 -0
- package/dist/core/agent/agent-registry.service.js +7 -0
- package/dist/core/agent/node-certificates-client.js +38 -0
- package/dist/core/auth/handoff-code.service.js +27 -4
- package/dist/core/moleculer/moleculer.service.js +5 -0
- package/package.json +9 -9
|
@@ -1,4 +1,37 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.enrichInputWithUserAgent = enrichInputWithUserAgent;
|
|
4
37
|
exports.enrichInputWithRelayClass = enrichInputWithRelayClass;
|
|
@@ -6,7 +39,10 @@ exports.stripTalkbackFromRequest = stripTalkbackFromRequest;
|
|
|
6
39
|
exports.enrichHintsWithClientClass = enrichHintsWithClientClass;
|
|
7
40
|
exports.wrapWebrtcSessionProviderWithRelay = wrapWebrtcSessionProviderWithRelay;
|
|
8
41
|
exports.buildAppRouter = buildAppRouter;
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
9
43
|
const addon_settings_router_js_1 = require("../core/addon-settings.router.js");
|
|
44
|
+
const certificates_router_js_1 = require("../core/certificates.router.js");
|
|
45
|
+
const node_certificates_client_js_1 = require("../../core/agent/node-certificates-client.js");
|
|
10
46
|
const auth_router_js_1 = require("../core/auth.router.js");
|
|
11
47
|
const cap_providers_js_1 = require("../core/cap-providers.js");
|
|
12
48
|
const capabilities_router_js_1 = require("../core/capabilities.router.js");
|
|
@@ -269,11 +305,20 @@ function buildCapabilityRouters(services) {
|
|
|
269
305
|
// clusterNodes — fixed core API. Write-side purge for the durable
|
|
270
306
|
// offline-node history (Track A "Forget node"); read side is push-only.
|
|
271
307
|
clusterNodes: (0, cluster_nodes_router_js_1.createClusterNodesRouter)(services.agentRegistry),
|
|
308
|
+
// certificates - Identity -> Certificates inventory (HTTPS per node, mesh CA). D855.
|
|
309
|
+
certificates: (0, certificates_router_js_1.createCertificatesRouter)({
|
|
310
|
+
registry: services.capabilityRegistry,
|
|
311
|
+
agentRegistry: services.agentRegistry,
|
|
312
|
+
nodeClient: (nodeId) => (0, node_certificates_client_js_1.createNodeCertificatesClient)(services.moleculer, nodeId),
|
|
313
|
+
hubDataDir: path.resolve(services.configService.get('server.dataPath') ?? 'camstack-data'),
|
|
314
|
+
log: services.loggingService.createLogger('certificates'),
|
|
315
|
+
}),
|
|
272
316
|
auth: (0, auth_router_js_1.createAuthRouter)(services.authService, services.capabilityRegistry, services.moleculer, services.shareTokenService, services.handoffCodeService, services.externalLinkIntents, services.nativeLink ?? null, {
|
|
273
317
|
...(services.authNetworkPolicy !== undefined
|
|
274
318
|
? { network: services.authNetworkPolicy }
|
|
275
319
|
: {}),
|
|
276
320
|
...(services.sessionBacking !== undefined ? { sessions: services.sessionBacking } : {}),
|
|
321
|
+
logger: services.loggingService.createLogger('auth'),
|
|
277
322
|
}),
|
|
278
323
|
// NOT MOUNTED — `mount: { kind: 'skip' }` legacy provider shapes
|
|
279
324
|
// (positional args / sync returns) that don't match the codegen
|
|
@@ -0,0 +1,148 @@
|
|
|
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.DeviceEnrollProofs = exports.DEVICE_ENROLL_PROOF_TTL_MS = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* Step-up proofs for `auth.enrollSelf` (D855).
|
|
39
|
+
*
|
|
40
|
+
* A signed-in user may request a device certificate for their own device, but
|
|
41
|
+
* only after signing in AGAIN at that moment — however recent their session
|
|
42
|
+
* is, and however many hours it has been refreshed for. The re-sign-in (a
|
|
43
|
+
* password / passkey re-authentication, or the app login handoff run in
|
|
44
|
+
* `step-up` purpose for any web method) yields ONE proof: purpose
|
|
45
|
+
* `device-enroll`, bound to the user, 5 minutes, single use, kept only as a
|
|
46
|
+
* hash. It replaces no session.
|
|
47
|
+
*/
|
|
48
|
+
const crypto = __importStar(require("node:crypto"));
|
|
49
|
+
exports.DEVICE_ENROLL_PROOF_TTL_MS = 5 * 60_000;
|
|
50
|
+
const MAX_PENDING = 1_000;
|
|
51
|
+
function hash(proof) {
|
|
52
|
+
return crypto.createHash('sha256').update(proof).digest('hex');
|
|
53
|
+
}
|
|
54
|
+
/** How long a spent reauth token / claimed session is remembered (past the token's own 5 min). */
|
|
55
|
+
const SPENT_MEMORY_MS = 6 * 60_000;
|
|
56
|
+
class DeviceEnrollProofs {
|
|
57
|
+
now;
|
|
58
|
+
logger;
|
|
59
|
+
pending = new Map();
|
|
60
|
+
spentReauth = new Map();
|
|
61
|
+
claimedSessions = new Map();
|
|
62
|
+
/**
|
|
63
|
+
* `claimedSessions` lives in memory, so a restart forgets which sign-ins already started a
|
|
64
|
+
* step-up. To keep "once per sign-in" true across a restart, a session OPENED BEFORE this
|
|
65
|
+
* process started is refused (it simply signs in again): the window is 120 s, so the cost
|
|
66
|
+
* is one re-login right after a hub restart.
|
|
67
|
+
*/
|
|
68
|
+
bootedAt;
|
|
69
|
+
constructor(now = Date.now, logger = null) {
|
|
70
|
+
this.now = now;
|
|
71
|
+
this.logger = logger;
|
|
72
|
+
this.bootedAt = now();
|
|
73
|
+
}
|
|
74
|
+
mint(userId) {
|
|
75
|
+
this.prune();
|
|
76
|
+
if (this.pending.size >= MAX_PENDING)
|
|
77
|
+
throw new Error('Too many pending step-up proofs');
|
|
78
|
+
const proof = `cdp_${crypto.randomBytes(32).toString('base64url')}`;
|
|
79
|
+
const expiresAt = this.now() + exports.DEVICE_ENROLL_PROOF_TTL_MS;
|
|
80
|
+
this.pending.set(hash(proof), { userId, expiresAt });
|
|
81
|
+
return { proof, expiresAt };
|
|
82
|
+
}
|
|
83
|
+
/** Consume `proof` for `userId`. Burned on the first attempt whatever the verdict. */
|
|
84
|
+
consume(proof, userId) {
|
|
85
|
+
if (proof === undefined || proof === '')
|
|
86
|
+
return this.refuse('missing', userId);
|
|
87
|
+
const key = hash(proof);
|
|
88
|
+
const entry = this.pending.get(key);
|
|
89
|
+
if (entry === undefined)
|
|
90
|
+
return this.refuse('unknown-or-used', userId);
|
|
91
|
+
this.pending.delete(key);
|
|
92
|
+
if (this.now() > entry.expiresAt)
|
|
93
|
+
return this.refuse('expired', userId);
|
|
94
|
+
if (entry.userId !== userId)
|
|
95
|
+
return this.refuse('other-user', userId);
|
|
96
|
+
return 'ok';
|
|
97
|
+
}
|
|
98
|
+
refuse(verdict, userId) {
|
|
99
|
+
this.logger?.warn(`Device enrollment step-up proof refused: ${verdict}`, {
|
|
100
|
+
tags: { userId },
|
|
101
|
+
meta: { reason: verdict },
|
|
102
|
+
});
|
|
103
|
+
return verdict;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A reauth token feeds ONE step-up (a proof or a step-up handoff code):
|
|
107
|
+
* `true` the first time, `false` for every later presentation.
|
|
108
|
+
*/
|
|
109
|
+
consumeReauth(reauthToken) {
|
|
110
|
+
this.prune();
|
|
111
|
+
const key = hash(reauthToken);
|
|
112
|
+
if (this.spentReauth.has(key)) {
|
|
113
|
+
this.logger?.warn('Reauth token already used for a step-up', {
|
|
114
|
+
meta: { reason: 'reauth-reused' },
|
|
115
|
+
});
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
this.spentReauth.set(key, this.now() + SPENT_MEMORY_MS);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
/** A freshly authenticated session may start ONE step-up: `true` the first time. */
|
|
122
|
+
claimFreshSession(sessionId, sessionCreatedAt) {
|
|
123
|
+
this.prune();
|
|
124
|
+
if (sessionCreatedAt < this.bootedAt) {
|
|
125
|
+
this.logger?.warn('Step-up refused: the sign-in predates this process, so a prior claim cannot be ruled out', {
|
|
126
|
+
meta: { reason: 'predates-boot' },
|
|
127
|
+
});
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (this.claimedSessions.has(sessionId))
|
|
131
|
+
return false;
|
|
132
|
+
this.claimedSessions.set(sessionId, this.now() + SPENT_MEMORY_MS);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
prune() {
|
|
136
|
+
const at = this.now();
|
|
137
|
+
for (const [k, exp] of this.spentReauth)
|
|
138
|
+
if (at > exp)
|
|
139
|
+
this.spentReauth.delete(k);
|
|
140
|
+
for (const [k, exp] of this.claimedSessions)
|
|
141
|
+
if (at > exp)
|
|
142
|
+
this.claimedSessions.delete(k);
|
|
143
|
+
for (const [k, v] of this.pending)
|
|
144
|
+
if (at > v.expiresAt)
|
|
145
|
+
this.pending.delete(k);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
exports.DeviceEnrollProofs = DeviceEnrollProofs;
|
|
@@ -30,6 +30,8 @@ function createSessionBacking(sessions, lifetimes) {
|
|
|
30
30
|
});
|
|
31
31
|
return { session, refreshToken };
|
|
32
32
|
},
|
|
33
|
+
revoke: (sessionId, reason) => sessions.revoke(sessionId, reason),
|
|
34
|
+
peek: (refreshToken) => sessions.peekByRefreshToken(refreshToken),
|
|
33
35
|
extend: async (sessionId) => sessions.extend(sessionId, lifetimes().sessionMs),
|
|
34
36
|
rotate: async (refreshToken) => {
|
|
35
37
|
const result = await sessions.refresh(refreshToken, lifetimes().sessionMs);
|
|
@@ -53,8 +53,9 @@ function createSessionMinter(deps) {
|
|
|
53
53
|
throw new SessionDirectoryUnavailableError();
|
|
54
54
|
return live;
|
|
55
55
|
};
|
|
56
|
-
const claims = (record, sid) => ({
|
|
56
|
+
const claims = (record, sid, cid) => ({
|
|
57
57
|
...(sid !== undefined ? { sid } : {}),
|
|
58
|
+
...(cid !== undefined && cid !== null ? { cid } : {}),
|
|
58
59
|
userId: record.id,
|
|
59
60
|
username: record.username,
|
|
60
61
|
isAdmin: record.isAdmin,
|
|
@@ -72,7 +73,9 @@ function createSessionMinter(deps) {
|
|
|
72
73
|
const remainingSec = Math.max(60, Math.floor((session.expiresAt - Date.now()) / 1000));
|
|
73
74
|
const expiresInSec = refreshable ? Math.min(live.accessSec(), remainingSec) : remainingSec;
|
|
74
75
|
return {
|
|
75
|
-
token: deps.signer.signToken(claims(record, session.id), {
|
|
76
|
+
token: deps.signer.signToken(claims(record, session.id, session.credentialId), {
|
|
77
|
+
expiresInSec,
|
|
78
|
+
}),
|
|
76
79
|
expiresAt: Date.now() + expiresInSec * 1000,
|
|
77
80
|
};
|
|
78
81
|
};
|
|
@@ -92,6 +95,7 @@ function createSessionMinter(deps) {
|
|
|
92
95
|
method: issue.method,
|
|
93
96
|
...(issue.client !== undefined ? { client: issue.client } : {}),
|
|
94
97
|
...(issue.ip !== undefined ? { ip: issue.ip } : {}),
|
|
98
|
+
...(issue.credentialId !== undefined ? { credentialId: issue.credentialId } : {}),
|
|
95
99
|
});
|
|
96
100
|
const access = signFor(record, opened.session, refreshable, live);
|
|
97
101
|
return {
|
|
@@ -132,6 +136,24 @@ function createSessionMinter(deps) {
|
|
|
132
136
|
const live = backing();
|
|
133
137
|
if (live === null)
|
|
134
138
|
throw new SessionRefreshRefusedError('unknown');
|
|
139
|
+
// A session opened by a device certificate lives only while that
|
|
140
|
+
// credential does (D855). The credential is judged BEFORE the refresh
|
|
141
|
+
// token is spent: a refusal here leaves the token untouched, so a transient
|
|
142
|
+
// `unavailable` can never turn into a "rotated-out token presented"
|
|
143
|
+
// refresh-reuse revocation on the retry. A revoked / expired credential
|
|
144
|
+
// revokes the session, so nothing is left to refresh.
|
|
145
|
+
const peeked = live.peek(refreshToken);
|
|
146
|
+
const credentialId = peeked?.credentialId ?? null;
|
|
147
|
+
if (peeked !== null && credentialId !== null) {
|
|
148
|
+
const gate = deps.credentials?.() ?? null;
|
|
149
|
+
const state = gate === null ? 'unavailable' : gate.check(credentialId);
|
|
150
|
+
if (state !== 'active') {
|
|
151
|
+
if (state === 'revoked' || state === 'expired') {
|
|
152
|
+
await live.revoke(peeked.id, `credential-${state}`);
|
|
153
|
+
}
|
|
154
|
+
throw new SessionRefreshRefusedError(`credential-${state}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
135
157
|
const rotated = await live.rotate(refreshToken);
|
|
136
158
|
if (!rotated.ok)
|
|
137
159
|
throw new SessionRefreshRefusedError(rotated.reason);
|
package/dist/auth/sso-routes.js
CHANGED
|
@@ -77,6 +77,7 @@ function registerSsoRoutes(fastify, deps) {
|
|
|
77
77
|
session = await deps.minter.mintForUserId(grant.userId, {
|
|
78
78
|
method: 'handoff',
|
|
79
79
|
refreshable: true,
|
|
80
|
+
...(grant.credentialId !== undefined ? { credentialId: grant.credentialId } : {}),
|
|
80
81
|
...(ip !== null ? { ip } : {}),
|
|
81
82
|
...clientOf(request.headers['user-agent']),
|
|
82
83
|
});
|
|
@@ -379,6 +379,13 @@ class AgentRegistryService {
|
|
|
379
379
|
// of failed deliveries to it is meaningless — drop it too.
|
|
380
380
|
this.backfillTracker.forget(nodeId);
|
|
381
381
|
this.deliveryLedger.forget(nodeId);
|
|
382
|
+
// The names the hub certified to this node are free again (a forgotten node cannot hold them).
|
|
383
|
+
await (0, system_1.getProcessAgentHttpsNames)()
|
|
384
|
+
?.release(nodeId)
|
|
385
|
+
.catch((err) => this.logger?.warn("forget cluster node: could not release the node's certified names", {
|
|
386
|
+
tags: { nodeId },
|
|
387
|
+
meta: { error: err instanceof Error ? err.message : String(err) },
|
|
388
|
+
}));
|
|
382
389
|
const historyRemoved = historyOutcome === 'removed';
|
|
383
390
|
const stillListed = this.isNodeKnown(nodeId);
|
|
384
391
|
const message = describeForgetOutcome(nodeId, historyOutcome, registryRemoved, stillListed);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNodeCertificatesClient = createNodeCertificatesClient;
|
|
4
|
+
/**
|
|
5
|
+
* The hub's typed handle on a node's `node-certificates` capability (D856).
|
|
6
|
+
*
|
|
7
|
+
* `createCapabilityProxy` hands back untyped functions; every answer is narrowed
|
|
8
|
+
* through the capability's own Zod output schema, and a missing proxy (node
|
|
9
|
+
* offline, capability not declared by an old agent) or a malformed answer becomes
|
|
10
|
+
* a plain error naming the node - never a cast.
|
|
11
|
+
*/
|
|
12
|
+
const types_1 = require("@camstack/types");
|
|
13
|
+
function createNodeCertificatesClient(moleculer, nodeId) {
|
|
14
|
+
const proxy = moleculer?.createCapabilityProxy('node-certificates', nodeId) ?? null;
|
|
15
|
+
if (proxy === null)
|
|
16
|
+
return null;
|
|
17
|
+
const call = async (method, schema, params) => {
|
|
18
|
+
const fn = proxy[method];
|
|
19
|
+
if (typeof fn !== 'function') {
|
|
20
|
+
throw new Error(`node ${nodeId} does not provide node-certificates.${method} (update the agent)`);
|
|
21
|
+
}
|
|
22
|
+
const parsed = schema.safeParse(await fn(params));
|
|
23
|
+
if (!parsed.success) {
|
|
24
|
+
throw new Error(`node ${nodeId} answered node-certificates.${method} with an unexpected shape`);
|
|
25
|
+
}
|
|
26
|
+
return parsed.data;
|
|
27
|
+
};
|
|
28
|
+
const m = types_1.nodeCertificatesCapability.methods;
|
|
29
|
+
return {
|
|
30
|
+
getCertificates: () => call('getCertificates', m.getCertificates.output, {}),
|
|
31
|
+
prepareMeshRenewal: () => call('prepareMeshRenewal', m.prepareMeshRenewal.output, {}),
|
|
32
|
+
installMeshLeaf: (input) => call('installMeshLeaf', m.installMeshLeaf.output, input),
|
|
33
|
+
confirmMeshLeaf: (input) => call('confirmMeshLeaf', m.confirmMeshLeaf.output, input),
|
|
34
|
+
rollbackMeshLeaf: () => call('rollbackMeshLeaf', m.rollbackMeshLeaf.output, {}),
|
|
35
|
+
prepareHttpsCertificate: () => call('prepareHttpsCertificate', m.prepareHttpsCertificate.output, {}),
|
|
36
|
+
installHttpsCertificate: (input) => call('installHttpsCertificate', m.installHttpsCertificate.output, input),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.HandoffCodeService = exports.HANDOFF_CODE_MAX_PENDING = exports.HANDOFF_CODE_TTL_MS = exports.HANDOFF_CODE_PREFIX = void 0;
|
|
37
|
+
exports.pkceChallenge = pkceChallenge;
|
|
37
38
|
/**
|
|
38
39
|
* HandoffCodeService — one-time login handoff codes for the native-app
|
|
39
40
|
* auth handoff (viewer "Sign in with CamStack").
|
|
@@ -64,6 +65,10 @@ exports.HANDOFF_CODE_TTL_MS = 60_000;
|
|
|
64
65
|
/** Hard bound on concurrently-pending codes (mint is auth-gated, so this
|
|
65
66
|
* only guards against a runaway authenticated client). */
|
|
66
67
|
exports.HANDOFF_CODE_MAX_PENDING = 1_000;
|
|
68
|
+
/** RFC 7636 S256: `base64url(sha256(verifier))`. */
|
|
69
|
+
function pkceChallenge(verifier) {
|
|
70
|
+
return crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
71
|
+
}
|
|
67
72
|
function hashCode(code) {
|
|
68
73
|
return crypto.createHash('sha256').update(code).digest('hex');
|
|
69
74
|
}
|
|
@@ -90,6 +95,9 @@ class HandoffCodeService {
|
|
|
90
95
|
userId,
|
|
91
96
|
...(options.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
|
|
92
97
|
purpose: options.purpose ?? 'renew',
|
|
98
|
+
...(options.codeChallenge !== undefined ? { codeChallenge: options.codeChallenge } : {}),
|
|
99
|
+
...(options.state !== undefined ? { state: options.state } : {}),
|
|
100
|
+
...(options.credentialId !== undefined ? { credentialId: options.credentialId } : {}),
|
|
93
101
|
expiresAt,
|
|
94
102
|
});
|
|
95
103
|
return { code, expiresAt };
|
|
@@ -97,15 +105,30 @@ class HandoffCodeService {
|
|
|
97
105
|
/** Redeem a raw code. Consumes the entry on the FIRST attempt no
|
|
98
106
|
* matter the outcome (single-use); returns `null` for unknown,
|
|
99
107
|
* already-used, or expired codes — the router maps that to 401. */
|
|
100
|
-
redeem(code) {
|
|
108
|
+
redeem(code, proof = {}) {
|
|
109
|
+
return this.redeemWithReason(code, proof).grant;
|
|
110
|
+
}
|
|
111
|
+
/** Like {@link redeem}, but says why a code was refused (for the log line). */
|
|
112
|
+
redeemWithReason(code, proof = {}) {
|
|
101
113
|
const entry = this.pending.get(hashCode(code));
|
|
102
114
|
if (!entry)
|
|
103
|
-
return null;
|
|
115
|
+
return { grant: null, reason: 'unknown-or-used' };
|
|
104
116
|
this.pending.delete(hashCode(code));
|
|
105
117
|
if (this.now() > entry.expiresAt)
|
|
106
|
-
return null;
|
|
118
|
+
return { grant: null, reason: 'expired' };
|
|
119
|
+
// PKCE: a code minted with a challenge redeems only with its verifier.
|
|
120
|
+
// The entry is already burned, so a wrong guess cannot be retried.
|
|
121
|
+
if (entry.codeChallenge !== undefined) {
|
|
122
|
+
if (proof.codeVerifier === undefined ||
|
|
123
|
+
pkceChallenge(proof.codeVerifier) !== entry.codeChallenge) {
|
|
124
|
+
return { grant: null, reason: 'pkce-mismatch' };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (entry.state !== undefined && proof.state !== entry.state) {
|
|
128
|
+
return { grant: null, reason: 'state-mismatch' };
|
|
129
|
+
}
|
|
107
130
|
const { expiresAt: _expiresAt, ...grant } = entry;
|
|
108
|
-
return grant;
|
|
131
|
+
return { grant, reason: null };
|
|
109
132
|
}
|
|
110
133
|
/** Number of not-yet-redeemed (possibly expired) codes. Test/diag aid. */
|
|
111
134
|
get pendingCount() {
|
|
@@ -711,6 +711,8 @@ class MoleculerService {
|
|
|
711
711
|
});
|
|
712
712
|
(0, system_1.setProcessMeshPeerTransportLookup)({
|
|
713
713
|
transportOf: (nodeId) => (0, system_1.getBrokerTlsTransporter)(this.broker)?.peerTransport(nodeId),
|
|
714
|
+
peerLeaf: (nodeId) => (0, system_1.getBrokerTlsTransporter)(this.broker)?.peerLeaf(nodeId),
|
|
715
|
+
dropPeer: (nodeId) => (0, system_1.getBrokerTlsTransporter)(this.broker)?.dropPeer(nodeId) ?? 0,
|
|
714
716
|
});
|
|
715
717
|
await this.brokerSafe.start();
|
|
716
718
|
logger.info('Moleculer broker started (TCP transport)');
|
|
@@ -927,6 +929,9 @@ class MoleculerService {
|
|
|
927
929
|
else
|
|
928
930
|
this.nodeOwners.set(params.nodeId, owner);
|
|
929
931
|
this.applyNodeManifest(params, previousManifest, owner, previousOwner);
|
|
932
|
+
// The node's capabilities are routable now: whoever waits for this registration (a mesh
|
|
933
|
+
// leaf renewal confirming the node came back WITH the new leaf) is told, with no polling.
|
|
934
|
+
(0, system_1.emitProcessNodeRegistered)(params.nodeId);
|
|
930
935
|
// Notify AgentRegistryService to reconcile placement for bare-ID
|
|
931
936
|
// agent nodes (no '/' = not a hub child worker, not the hub itself).
|
|
932
937
|
// The handshake is the authoritative completeness signal — the full
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.565",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -38,19 +38,19 @@
|
|
|
38
38
|
]
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
42
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
43
|
-
"@camstack/addon-auth": "1.2.
|
|
41
|
+
"@camstack/addon-admin-ui": "1.2.419",
|
|
42
|
+
"@camstack/addon-agent-ui": "1.2.231",
|
|
43
|
+
"@camstack/addon-auth": "1.2.144",
|
|
44
44
|
"@camstack/addon-decoder-nodeav": "1.2.123",
|
|
45
45
|
"@camstack/addon-notifiers": "1.2.134",
|
|
46
|
-
"@camstack/addon-pipeline": "1.2.
|
|
47
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
46
|
+
"@camstack/addon-pipeline": "1.2.368",
|
|
47
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.318",
|
|
48
48
|
"@camstack/addon-post-analysis": "1.2.369",
|
|
49
49
|
"@camstack/develop": "0.1.8",
|
|
50
50
|
"@camstack/sdk": "1.2.131",
|
|
51
|
-
"@camstack/system": "1.2.
|
|
52
|
-
"@camstack/types": "1.2.
|
|
53
|
-
"@camstack/ui-library": "1.2.
|
|
51
|
+
"@camstack/system": "1.2.429",
|
|
52
|
+
"@camstack/types": "1.2.365",
|
|
53
|
+
"@camstack/ui-library": "1.2.288",
|
|
54
54
|
"@fastify/compress": "^9.0.0",
|
|
55
55
|
"@fastify/cookie": "^11.0.2",
|
|
56
56
|
"@fastify/cors": "^11.2.0",
|