@adobe/helix-html-pipeline 3.8.11 → 3.9.0
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/CHANGELOG.md +14 -0
- package/package.json +6 -6
- package/src/PipelineState.d.ts +4 -0
- package/src/html-pipe.js +6 -3
- package/src/json-pipe.js +50 -29
- package/src/steps/authenticate.js +39 -0
- package/src/utils/json-filter.js +15 -46
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [3.9.0](https://github.com/adobe/helix-html-pipeline/compare/v3.8.12...v3.9.0) (2023-03-23)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* restrict repositories ([#281](https://github.com/adobe/helix-html-pipeline/issues/281)) ([ba7e670](https://github.com/adobe/helix-html-pipeline/commit/ba7e670a0c2fe5e331c37000d0c023cfb79961ce)), closes [#277](https://github.com/adobe/helix-html-pipeline/issues/277)
|
|
7
|
+
|
|
8
|
+
## [3.8.12](https://github.com/adobe/helix-html-pipeline/compare/v3.8.11...v3.8.12) (2023-03-10)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* ensure the .md is delivered ([#275](https://github.com/adobe/helix-html-pipeline/issues/275)) ([c22ab6b](https://github.com/adobe/helix-html-pipeline/commit/c22ab6b23817fa5d19a6563e2fc0b4a1f201a04f))
|
|
14
|
+
|
|
1
15
|
## [3.8.11](https://github.com/adobe/helix-html-pipeline/compare/v3.8.10...v3.8.11) (2023-03-02)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-html-pipeline",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "Helix HTML Pipeline",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"hast-util-to-html": "8.0.4",
|
|
51
51
|
"hast-util-to-string": "2.0.0",
|
|
52
52
|
"hastscript": "7.2.0",
|
|
53
|
-
"jose": "4.
|
|
53
|
+
"jose": "4.13.1",
|
|
54
54
|
"mdast-util-gfm-footnote": "1.0.2",
|
|
55
55
|
"mdast-util-gfm-strikethrough": "1.0.3",
|
|
56
56
|
"mdast-util-gfm-table": "1.0.7",
|
|
@@ -83,20 +83,20 @@
|
|
|
83
83
|
"@semantic-release/git": "10.0.1",
|
|
84
84
|
"@semantic-release/npm": "9.0.2",
|
|
85
85
|
"c8": "7.13.0",
|
|
86
|
-
"eslint": "8.
|
|
86
|
+
"eslint": "8.36.0",
|
|
87
87
|
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
|
88
88
|
"eslint-plugin-header": "3.1.1",
|
|
89
89
|
"eslint-plugin-import": "2.27.5",
|
|
90
90
|
"esmock": "2.1.0",
|
|
91
91
|
"husky": "8.0.3",
|
|
92
92
|
"js-yaml": "4.1.0",
|
|
93
|
-
"jsdom": "21.1.
|
|
93
|
+
"jsdom": "21.1.1",
|
|
94
94
|
"junit-report-builder": "3.0.1",
|
|
95
|
-
"lint-staged": "13.
|
|
95
|
+
"lint-staged": "13.2.0",
|
|
96
96
|
"mocha": "10.2.0",
|
|
97
97
|
"mocha-multi-reporters": "1.5.1",
|
|
98
98
|
"remark-gfm": "3.0.1",
|
|
99
|
-
"semantic-release": "20.1.
|
|
99
|
+
"semantic-release": "20.1.3"
|
|
100
100
|
},
|
|
101
101
|
"lint-staged": {
|
|
102
102
|
"*.js": "eslint",
|
package/src/PipelineState.d.ts
CHANGED
package/src/html-pipe.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
|
|
13
|
-
import { authenticate } from './steps/authenticate.js';
|
|
13
|
+
import { authenticate, requireProject } from './steps/authenticate.js';
|
|
14
14
|
import addHeadingIds from './steps/add-heading-ids.js';
|
|
15
15
|
import createPageBlocks from './steps/create-page-blocks.js';
|
|
16
16
|
import createPictures from './steps/create-pictures.js';
|
|
@@ -91,7 +91,10 @@ export async function htmlPipe(state, req) {
|
|
|
91
91
|
fetchContent(state, req, res),
|
|
92
92
|
]);
|
|
93
93
|
|
|
94
|
-
await
|
|
94
|
+
await requireProject(state, req, res);
|
|
95
|
+
if (!res.error) {
|
|
96
|
+
await authenticate(state, req, res);
|
|
97
|
+
}
|
|
95
98
|
|
|
96
99
|
if (res.error) {
|
|
97
100
|
// if content loading produced an error, we're done.
|
|
@@ -104,7 +107,7 @@ export async function htmlPipe(state, req) {
|
|
|
104
107
|
return res;
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
if (state.content.sourceBus === 'code') {
|
|
110
|
+
if (state.content.sourceBus === 'code' || state.info.originalExtension === '.md') {
|
|
108
111
|
state.timer?.update('serialize');
|
|
109
112
|
await renderCode(state, req, res);
|
|
110
113
|
} else {
|
package/src/json-pipe.js
CHANGED
|
@@ -42,6 +42,31 @@ export default function folderMapping(state) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
async function fetchJsonContent(state, req, res) {
|
|
46
|
+
const {
|
|
47
|
+
owner, repo, ref, contentBusId, partition, s3Loader, log,
|
|
48
|
+
} = state;
|
|
49
|
+
const { path } = state.info;
|
|
50
|
+
let ret = await s3Loader.getObject('helix-content-bus', `${contentBusId}/${partition}${path}`);
|
|
51
|
+
|
|
52
|
+
// if not found, fall back to code bus
|
|
53
|
+
if (ret.status === 404) {
|
|
54
|
+
ret = await s3Loader.getObject('helix-code-bus', `${owner}/${repo}/${ref}${path}`);
|
|
55
|
+
}
|
|
56
|
+
if (ret.status === 200) {
|
|
57
|
+
state.content.data = ret.body;
|
|
58
|
+
|
|
59
|
+
// store extra source location if present
|
|
60
|
+
state.content.sourceLocation = ret.headers.get('x-amz-meta-x-source-location');
|
|
61
|
+
log.info(`source-location: ${state.content.sourceLocation}`);
|
|
62
|
+
|
|
63
|
+
updateLastModified(state, res, extractLastModified(ret.headers));
|
|
64
|
+
} else {
|
|
65
|
+
res.status = ret.status === 404 ? 404 : 502;
|
|
66
|
+
res.error = `failed to load ${state.info.resourcePath}: ${ret.status}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
/**
|
|
46
71
|
* Runs the default pipeline and returns the response.
|
|
47
72
|
* @param {PipelineState} state
|
|
@@ -51,9 +76,7 @@ export default function folderMapping(state) {
|
|
|
51
76
|
export async function jsonPipe(state, req) {
|
|
52
77
|
const { log } = state;
|
|
53
78
|
state.type = 'json';
|
|
54
|
-
const {
|
|
55
|
-
owner, repo, ref, contentBusId, partition, s3Loader,
|
|
56
|
-
} = state;
|
|
79
|
+
const { contentBusId } = state;
|
|
57
80
|
const { extension } = state.info;
|
|
58
81
|
const { searchParams } = req.url;
|
|
59
82
|
const params = Object.fromEntries(searchParams.entries());
|
|
@@ -81,56 +104,54 @@ export async function jsonPipe(state, req) {
|
|
|
81
104
|
await fetchConfig(state, req);
|
|
82
105
|
await folderMapping(state);
|
|
83
106
|
|
|
84
|
-
|
|
85
|
-
const
|
|
107
|
+
/** @type PipelineResponse */
|
|
108
|
+
const res = new PipelineResponse('', {
|
|
109
|
+
headers: {
|
|
110
|
+
'content-type': 'application/json',
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
86
114
|
state.timer?.update('json-fetch');
|
|
87
|
-
|
|
115
|
+
await Promise.all([
|
|
116
|
+
fetchConfigAll(state, req, res),
|
|
117
|
+
fetchJsonContent(state, req, res),
|
|
118
|
+
]);
|
|
88
119
|
|
|
89
|
-
|
|
90
|
-
if (dataResponse.status === 404) {
|
|
91
|
-
dataResponse = await s3Loader.getObject('helix-code-bus', `${owner}/${repo}/${ref}${path}`);
|
|
92
|
-
}
|
|
120
|
+
await authenticate(state, req, res);
|
|
93
121
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (dataResponse.status < 500) {
|
|
97
|
-
await fetchConfigAll(state, req, dataResponse);
|
|
98
|
-
await setCustomResponseHeaders(state, req, dataResponse);
|
|
99
|
-
}
|
|
100
|
-
return dataResponse;
|
|
122
|
+
if (res.error) {
|
|
123
|
+
throw new PipelineStatusError(res.status, res.error);
|
|
101
124
|
}
|
|
102
|
-
const data = dataResponse.body;
|
|
103
125
|
|
|
104
126
|
// filter data
|
|
105
|
-
|
|
127
|
+
jsonFilter(state, res, {
|
|
106
128
|
limit: limit ? Number.parseInt(limit, 10) : undefined,
|
|
107
129
|
offset: offset ? Number.parseInt(offset, 10) : undefined,
|
|
108
130
|
sheet,
|
|
109
131
|
raw: limit === undefined && offset === undefined && sheet === undefined,
|
|
110
132
|
});
|
|
111
133
|
|
|
112
|
-
// set last-modified
|
|
113
|
-
updateLastModified(state, response, extractLastModified(dataResponse.headers));
|
|
114
|
-
|
|
115
134
|
// set surrogate keys
|
|
116
135
|
const keys = [];
|
|
136
|
+
const { path } = state.info;
|
|
117
137
|
keys.push(`${contentBusId}${path}`.replace(/\//g, '_')); // TODO: remove
|
|
118
138
|
keys.push(await computeSurrogateKey(`${contentBusId}${path}`));
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// Load config-all and set response headers
|
|
122
|
-
await fetchConfigAll(state, req, response);
|
|
123
|
-
await authenticate(state, req, response);
|
|
124
|
-
await setCustomResponseHeaders(state, req, response);
|
|
139
|
+
res.headers.set('x-surrogate-key', keys.join(' '));
|
|
125
140
|
|
|
126
|
-
|
|
141
|
+
await setCustomResponseHeaders(state, req, res);
|
|
142
|
+
return res;
|
|
127
143
|
} catch (e) {
|
|
128
144
|
const res = new PipelineResponse('', {
|
|
129
145
|
status: e instanceof PipelineStatusError ? e.code : 500,
|
|
130
146
|
});
|
|
131
147
|
const level = res.status >= 500 ? 'error' : 'info';
|
|
132
148
|
log[level](`pipeline status: ${res.status} ${e.message}`, e);
|
|
149
|
+
res.body = '';
|
|
133
150
|
res.headers.set('x-error', cleanupHeaderValue(e.message));
|
|
151
|
+
if (res.status < 500) {
|
|
152
|
+
await setCustomResponseHeaders(state, req, res);
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
return res;
|
|
135
156
|
}
|
|
136
157
|
}
|
|
@@ -82,3 +82,42 @@ export async function authenticate(state, req, res) {
|
|
|
82
82
|
res.headers.set('x-hlx-auth-key', authInfo.profile.pem);
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Checks if the given owner repo is alloed
|
|
88
|
+
* @param {string} owner
|
|
89
|
+
* @param {string} repo
|
|
90
|
+
* @param {string[]} allows
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
export function isOwnerRepoAllowed(owner, repo, allows = []) {
|
|
94
|
+
if (allows.length === 0) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return allows
|
|
98
|
+
.map((ownerRepo) => ownerRepo.split('/'))
|
|
99
|
+
.findIndex(([o, r]) => owner === o && (repo === r || r === '*')) >= 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Checks if the
|
|
104
|
+
* @type PipelineStep
|
|
105
|
+
* @param {PipelineState} state
|
|
106
|
+
* @param {PipelineRequest} req
|
|
107
|
+
* @param {PipelineResponse} res
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
*/
|
|
110
|
+
export async function requireProject(state, req, res) {
|
|
111
|
+
// if not restricted, do nothing
|
|
112
|
+
const ownerRepo = state.config?.access?.require?.repository;
|
|
113
|
+
if (!ownerRepo) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const ownerRepos = Array.isArray(ownerRepo) ? ownerRepo : [ownerRepo];
|
|
117
|
+
const { log, owner, repo } = state;
|
|
118
|
+
if (!isOwnerRepoAllowed(owner, repo, ownerRepos)) {
|
|
119
|
+
log.warn(`${owner}/${repo} not allowed for ${ownerRepos}`);
|
|
120
|
+
res.status = 403;
|
|
121
|
+
res.error = 'forbidden.';
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/utils/json-filter.js
CHANGED
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import { PipelineResponse } from '../PipelineResponse.js';
|
|
12
|
+
import { PipelineStatusError } from '../PipelineStatusError.js';
|
|
14
13
|
|
|
15
14
|
const TYPE_KEY = ':type';
|
|
16
15
|
|
|
@@ -21,11 +20,10 @@ const NAMES_KEY = ':names';
|
|
|
21
20
|
/**
|
|
22
21
|
* Creates a json response from the given data and query
|
|
23
22
|
* @param {PipelineState} state
|
|
24
|
-
* @param {
|
|
23
|
+
* @param {PipelineResponse} res
|
|
25
24
|
* @param {object} query
|
|
26
|
-
* @returns {PipelineResponse}
|
|
27
25
|
*/
|
|
28
|
-
export default function jsonFilter(state,
|
|
26
|
+
export default function jsonFilter(state, res, query) {
|
|
29
27
|
const {
|
|
30
28
|
limit = 1000,
|
|
31
29
|
offset = 0,
|
|
@@ -45,37 +43,26 @@ export default function jsonFilter(state, data, query) {
|
|
|
45
43
|
};
|
|
46
44
|
}
|
|
47
45
|
|
|
46
|
+
const { data } = state.content;
|
|
48
47
|
let json;
|
|
49
48
|
try {
|
|
50
49
|
state.timer?.update('json-parse');
|
|
51
50
|
json = JSON.parse(data);
|
|
52
51
|
} catch (e) {
|
|
53
52
|
const msg = `failed to parse json: ${e.message}`;
|
|
54
|
-
if (raw) {
|
|
55
|
-
|
|
56
|
-
return new PipelineResponse(data, {
|
|
57
|
-
headers: {
|
|
58
|
-
'content-type': 'text/plain',
|
|
59
|
-
},
|
|
60
|
-
});
|
|
53
|
+
if (!raw) {
|
|
54
|
+
throw new PipelineStatusError(502, msg);
|
|
61
55
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
headers: {
|
|
67
|
-
'x-error': cleanupHeaderValue(msg),
|
|
68
|
-
},
|
|
69
|
-
});
|
|
56
|
+
log.warn(msg);
|
|
57
|
+
res.body = data;
|
|
58
|
+
res.headers.set('content-type', 'text/plain; charset=utf-8');
|
|
59
|
+
return;
|
|
70
60
|
}
|
|
71
61
|
|
|
72
62
|
// when raw request, only handle multisheets.
|
|
73
63
|
if (raw && !(NAMES_KEY in json)) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
'content-type': 'application/json',
|
|
77
|
-
},
|
|
78
|
-
});
|
|
64
|
+
res.body = data;
|
|
65
|
+
return;
|
|
79
66
|
}
|
|
80
67
|
|
|
81
68
|
// if single sheet, convert it to multisheet
|
|
@@ -87,14 +74,7 @@ export default function jsonFilter(state, data, query) {
|
|
|
87
74
|
}
|
|
88
75
|
|
|
89
76
|
if (!json[NAMES_KEY]) {
|
|
90
|
-
|
|
91
|
-
log.error(msg);
|
|
92
|
-
return new PipelineResponse('', {
|
|
93
|
-
status: 502,
|
|
94
|
-
headers: {
|
|
95
|
-
'x-error': cleanupHeaderValue(msg),
|
|
96
|
-
},
|
|
97
|
-
});
|
|
77
|
+
throw new PipelineStatusError(502, 'multisheet data invalid. missing ":names" property.');
|
|
98
78
|
}
|
|
99
79
|
|
|
100
80
|
state.timer?.update('json-filter');
|
|
@@ -111,14 +91,7 @@ export default function jsonFilter(state, data, query) {
|
|
|
111
91
|
sheetNames.push(name);
|
|
112
92
|
});
|
|
113
93
|
if (sheetNames.length === 0 && requestedSheets.length > 0) {
|
|
114
|
-
|
|
115
|
-
log.info(msg);
|
|
116
|
-
return new PipelineResponse('', {
|
|
117
|
-
status: 404,
|
|
118
|
-
headers: {
|
|
119
|
-
'x-error': cleanupHeaderValue(msg),
|
|
120
|
-
},
|
|
121
|
-
});
|
|
94
|
+
throw new PipelineStatusError(404, `filtered result does not contain selected sheet(s): ${requestedSheets.join(',')}`);
|
|
122
95
|
}
|
|
123
96
|
|
|
124
97
|
let body;
|
|
@@ -135,9 +108,5 @@ export default function jsonFilter(state, data, query) {
|
|
|
135
108
|
}
|
|
136
109
|
body[TYPE_KEY] = type;
|
|
137
110
|
state.timer?.update('json-stringify');
|
|
138
|
-
|
|
139
|
-
headers: {
|
|
140
|
-
'content-type': 'application/json',
|
|
141
|
-
},
|
|
142
|
-
});
|
|
111
|
+
res.body = JSON.stringify(body);
|
|
143
112
|
}
|