@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 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.8.11",
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.12.0",
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.34.0",
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.0",
93
+ "jsdom": "21.1.1",
94
94
  "junit-report-builder": "3.0.1",
95
- "lint-staged": "13.1.2",
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.0"
99
+ "semantic-release": "20.1.3"
100
100
  },
101
101
  "lint-staged": {
102
102
  "*.js": "eslint",
@@ -23,6 +23,10 @@ type Fetch = (url: string|Request, options?: RequestOptions) => Promise<Response
23
23
 
24
24
  declare interface AccessConfig {
25
25
  allow:(string|string[]);
26
+
27
+ require: {
28
+ repository:(string|string[]);
29
+ };
26
30
  }
27
31
 
28
32
  declare interface HelixConfigAll {
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 authenticate(state, req, res);
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
- // fetch data from content bus
85
- const { path } = state.info;
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
- let dataResponse = await s3Loader.getObject('helix-content-bus', `${contentBusId}/${partition}${path}`);
115
+ await Promise.all([
116
+ fetchConfigAll(state, req, res),
117
+ fetchJsonContent(state, req, res),
118
+ ]);
88
119
 
89
- // if not found, fall back to code bus
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
- // if still not found, return status
95
- if (dataResponse.status !== 200) {
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
- const response = jsonFilter(state, data, {
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
- response.headers.set('x-surrogate-key', keys.join(' '));
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
- return response;
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
+ }
@@ -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 { cleanupHeaderValue } from '@adobe/helix-shared-utils';
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 {object} data
23
+ * @param {PipelineResponse} res
25
24
  * @param {object} query
26
- * @returns {PipelineResponse}
27
25
  */
28
- export default function jsonFilter(state, data, query) {
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
- log.warn(msg);
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
- log.error(msg);
64
- return new PipelineResponse('', {
65
- status: 502,
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
- return new PipelineResponse(data, {
75
- headers: {
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
- const msg = 'multisheet data invalid. missing ":names" property.';
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
- const msg = `filtered result does not contain selected sheet(s): ${requestedSheets.join(',')}`;
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
- return new PipelineResponse(JSON.stringify(body), {
139
- headers: {
140
- 'content-type': 'application/json',
141
- },
142
- });
111
+ res.body = JSON.stringify(body);
143
112
  }