@camstack/server 1.2.80 → 1.2.81
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.
|
@@ -7,6 +7,43 @@ exports.registerOauth2Routes = registerOauth2Routes;
|
|
|
7
7
|
const session_cookie_js_1 = require("../../auth/session-cookie.js");
|
|
8
8
|
const consent_page_js_1 = require("./consent-page.js");
|
|
9
9
|
const private_host_js_1 = require("./private-host.js");
|
|
10
|
+
/** Longest caller-supplied `integration` value echoed back in an error. */
|
|
11
|
+
const MAX_ECHOED_INTEGRATION_LEN = 100;
|
|
12
|
+
/** Render the received `integration` for a human, bounded. */
|
|
13
|
+
function describeReceivedIntegration(raw) {
|
|
14
|
+
if (raw === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
const joined = typeof raw === 'string' ? raw : raw.join(',');
|
|
17
|
+
if (joined === '')
|
|
18
|
+
return null;
|
|
19
|
+
return joined.length > MAX_ECHOED_INTEGRATION_LEN
|
|
20
|
+
? `${joined.slice(0, MAX_ECHOED_INTEGRATION_LEN)}…`
|
|
21
|
+
: joined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the `integration` parameter to a single id.
|
|
25
|
+
*
|
|
26
|
+
* A repeat carrying ONE distinct value is collapsed rather than refused:
|
|
27
|
+
* every layer that parses the query — proxy, framework, app — necessarily
|
|
28
|
+
* agrees on the value, so there is no parameter-pollution ambiguity to
|
|
29
|
+
* protect against, and refusing would strand a client whose request is
|
|
30
|
+
* merely redundant. A repeat whose values DISAGREE is exactly that
|
|
31
|
+
* ambiguity, and is refused.
|
|
32
|
+
*/
|
|
33
|
+
function resolveIntegrationParam(raw) {
|
|
34
|
+
if (raw === undefined)
|
|
35
|
+
return { ok: false, reason: 'missing' };
|
|
36
|
+
if (typeof raw === 'string') {
|
|
37
|
+
return raw === '' ? { ok: false, reason: 'missing' } : { ok: true, value: raw };
|
|
38
|
+
}
|
|
39
|
+
const distinct = [...new Set(raw.filter((v) => v !== ''))];
|
|
40
|
+
const [first] = distinct;
|
|
41
|
+
if (first === undefined)
|
|
42
|
+
return { ok: false, reason: 'missing' };
|
|
43
|
+
if (distinct.length > 1)
|
|
44
|
+
return { ok: false, reason: 'conflicting-repeat' };
|
|
45
|
+
return { ok: true, value: first };
|
|
46
|
+
}
|
|
10
47
|
/** Validate the inbound authorize query. `client_id` is intentionally
|
|
11
48
|
* NOT checked — that pair is verified only at the Lambda boundary, and a
|
|
12
49
|
* PUBLIC client (`requiresPkce`) has no secret to check it against at all;
|
|
@@ -14,9 +51,24 @@ const private_host_js_1 = require("./private-host.js");
|
|
|
14
51
|
function validateAuthorizeQuery(q, knownIntegrations) {
|
|
15
52
|
if (q.response_type !== 'code')
|
|
16
53
|
return { ok: false, status: 400, error: 'unsupported_response_type' };
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
54
|
+
// Three distinct client faults used to collapse into one opaque string.
|
|
55
|
+
// Alexa linking was down for a day because the message named neither what
|
|
56
|
+
// arrived nor what would have been accepted.
|
|
57
|
+
const detail = {
|
|
58
|
+
received: describeReceivedIntegration(q.integration),
|
|
59
|
+
known_integrations: [...knownIntegrations.keys()],
|
|
60
|
+
};
|
|
61
|
+
const param = resolveIntegrationParam(q.integration);
|
|
62
|
+
if (!param.ok) {
|
|
63
|
+
const error = param.reason === 'missing'
|
|
64
|
+
? 'invalid_request — integration parameter missing'
|
|
65
|
+
: 'invalid_request — integration parameter repeated with conflicting values';
|
|
66
|
+
return { ok: false, status: 400, error, detail };
|
|
67
|
+
}
|
|
68
|
+
const policy = knownIntegrations.get(param.value);
|
|
69
|
+
if (!policy) {
|
|
70
|
+
return { ok: false, status: 400, error: 'invalid_request — unknown integration', detail };
|
|
71
|
+
}
|
|
20
72
|
if (!q.redirect_uri)
|
|
21
73
|
return { ok: false, status: 400, error: 'invalid_request — redirect_uri required' };
|
|
22
74
|
if (!q.state)
|
|
@@ -32,7 +84,7 @@ function validateAuthorizeQuery(q, knownIntegrations) {
|
|
|
32
84
|
}
|
|
33
85
|
return {
|
|
34
86
|
ok: true,
|
|
35
|
-
integration:
|
|
87
|
+
integration: param.value,
|
|
36
88
|
redirectUri: q.redirect_uri,
|
|
37
89
|
state: q.state,
|
|
38
90
|
codeChallenge: challenge,
|
|
@@ -76,6 +128,20 @@ function summariseScopes(scopes) {
|
|
|
76
128
|
}
|
|
77
129
|
return scopes.map((s) => s.type).join(', ') || 'no permissions';
|
|
78
130
|
}
|
|
131
|
+
/** A refused `/authorize`. Logged at `warn` and echoed to the caller: both
|
|
132
|
+
* halves name the value that ARRIVED and the ids that would have worked, so
|
|
133
|
+
* neither the operator nor the log reader has to guess which of them is
|
|
134
|
+
* wrong. */
|
|
135
|
+
function refuseAuthorize(reply, logger, method, refusal) {
|
|
136
|
+
logger.warn(`oauth2 authorize refused: ${refusal.error}`, {
|
|
137
|
+
meta: {
|
|
138
|
+
method,
|
|
139
|
+
received: refusal.detail?.received ?? null,
|
|
140
|
+
knownIntegrations: refusal.detail?.known_integrations ?? [],
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return reply.status(refusal.status).send({ error: refusal.error, ...refusal.detail });
|
|
144
|
+
}
|
|
79
145
|
/** Build a map of integrationId → descriptor from all registered oauth-integration providers. */
|
|
80
146
|
async function buildIntegrationMap(registry) {
|
|
81
147
|
const entries = registry.getCollectionEntries('oauth-integration');
|
|
@@ -137,10 +203,19 @@ function registerOauth2Routes(fastify, deps) {
|
|
|
137
203
|
const query = request.query;
|
|
138
204
|
const v = validateAuthorizeQuery(query, descriptorMap);
|
|
139
205
|
if (!v.ok) {
|
|
140
|
-
return reply.
|
|
206
|
+
return refuseAuthorize(reply, deps.logger, 'GET', v);
|
|
141
207
|
}
|
|
142
208
|
const descriptor = descriptorMap.get(v.integration);
|
|
143
209
|
if (!isRedirectUriAllowed(v.redirectUri, descriptor.allowedRedirectPrefixes, descriptor.allowedPrivateHostPaths ?? [])) {
|
|
210
|
+
deps.logger.warn('oauth2 authorize refused: redirect_uri not allowed', {
|
|
211
|
+
meta: {
|
|
212
|
+
method: 'GET',
|
|
213
|
+
integration: v.integration,
|
|
214
|
+
redirectUri: v.redirectUri,
|
|
215
|
+
allowedPrefixes: descriptor.allowedRedirectPrefixes,
|
|
216
|
+
allowedPrivateHostPaths: descriptor.allowedPrivateHostPaths ?? [],
|
|
217
|
+
},
|
|
218
|
+
});
|
|
144
219
|
return reply
|
|
145
220
|
.status(400)
|
|
146
221
|
.send({ error: 'invalid_request — redirect_uri not allowed for this integration' });
|
|
@@ -223,10 +298,19 @@ function registerOauth2Routes(fastify, deps) {
|
|
|
223
298
|
// and every one of them is attacker-editable.
|
|
224
299
|
const v = validateAuthorizeQuery(formQuery, descriptorMap);
|
|
225
300
|
if (!v.ok) {
|
|
226
|
-
return reply.
|
|
301
|
+
return refuseAuthorize(reply, deps.logger, 'POST', v);
|
|
227
302
|
}
|
|
228
303
|
const descriptor = descriptorMap.get(v.integration);
|
|
229
304
|
if (!isRedirectUriAllowed(v.redirectUri, descriptor.allowedRedirectPrefixes, descriptor.allowedPrivateHostPaths ?? [])) {
|
|
305
|
+
deps.logger.warn('oauth2 authorize refused: redirect_uri not allowed', {
|
|
306
|
+
meta: {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
integration: v.integration,
|
|
309
|
+
redirectUri: v.redirectUri,
|
|
310
|
+
allowedPrefixes: descriptor.allowedRedirectPrefixes,
|
|
311
|
+
allowedPrivateHostPaths: descriptor.allowedPrivateHostPaths ?? [],
|
|
312
|
+
},
|
|
313
|
+
});
|
|
230
314
|
return reply
|
|
231
315
|
.status(400)
|
|
232
316
|
.send({ error: 'invalid_request — redirect_uri not allowed for this integration' });
|
|
@@ -7794,6 +7794,15 @@ function createCapRouter_snapshot(getProvider, createRemoteProxy) {
|
|
|
7794
7794
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
7795
7795
|
return p.getSnapshotOverview(methodInput);
|
|
7796
7796
|
}),
|
|
7797
|
+
getSnapshotLinks: trpc_middleware_js_1.protectedProcedure
|
|
7798
|
+
.input(types_98.snapshotCapability.methods.getSnapshotLinks.input.loose())
|
|
7799
|
+
.output(types_98.snapshotCapability.methods.getSnapshotLinks.output)
|
|
7800
|
+
.query(async ({ input, ctx }) => {
|
|
7801
|
+
const { nodeId, ...methodInput } = input;
|
|
7802
|
+
const p = resolveProvider('snapshot', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
7803
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
7804
|
+
return p.getSnapshotLinks(methodInput);
|
|
7805
|
+
}),
|
|
7797
7806
|
});
|
|
7798
7807
|
}
|
|
7799
7808
|
function createCapRouter_ssoBridge(getProvider, createRemoteProxy) {
|
package/dist/main.js
CHANGED
|
@@ -958,6 +958,10 @@ async function bootstrap() {
|
|
|
958
958
|
getRegistry: () => capabilityRegistry,
|
|
959
959
|
verifyToken: (t) => authService.verifyToken(t),
|
|
960
960
|
publicHubUrl: () => process.env.CAMSTACK_PUBLIC_ORIGIN ?? `https://localhost:${port}`,
|
|
961
|
+
// Every /authorize refusal is a dropped account-linking attempt. Without
|
|
962
|
+
// this scope the drop is invisible in Loki and the only signal left is an
|
|
963
|
+
// operator noticing that linking stopped working.
|
|
964
|
+
logger: app.get(logging_service_1.LoggingService).createLogger('oauth2'),
|
|
961
965
|
});
|
|
962
966
|
console.log('[bootstrap] OAuth2 routes registered at /api/oauth2/*');
|
|
963
967
|
// Attach tRPC WebSocket handler using noServer mode to avoid
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.81",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"@camstack/addon-notifiers": "1.2.13",
|
|
41
41
|
"@camstack/addon-pipeline": "1.2.52",
|
|
42
42
|
"@camstack/addon-pipeline-orchestrator": "1.2.33",
|
|
43
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
43
|
+
"@camstack/addon-post-analysis": "1.2.56",
|
|
44
44
|
"@camstack/sdk": "1.2.10",
|
|
45
45
|
"@camstack/shm-ring": "1.1.9",
|
|
46
|
-
"@camstack/system": "1.2.
|
|
47
|
-
"@camstack/types": "1.2.
|
|
48
|
-
"@camstack/ui-library": "1.2.
|
|
46
|
+
"@camstack/system": "1.2.68",
|
|
47
|
+
"@camstack/types": "1.2.52",
|
|
48
|
+
"@camstack/ui-library": "1.2.37",
|
|
49
49
|
"@fastify/compress": "^9.0.0",
|
|
50
50
|
"@fastify/cookie": "^11.0.2",
|
|
51
51
|
"@fastify/cors": "^11.2.0",
|