@adobe/helix-html-pipeline 5.4.4 → 6.0.1

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,22 @@
1
+ ## [6.0.1](https://github.com/adobe/helix-html-pipeline/compare/v6.0.0...v6.0.1) (2024-01-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * enforce rso ([#488](https://github.com/adobe/helix-html-pipeline/issues/488)) ([4b38e25](https://github.com/adobe/helix-html-pipeline/commit/4b38e2524514a3d64a041d456bf14fe8b2e416cb))
7
+
8
+ # [6.0.0](https://github.com/adobe/helix-html-pipeline/compare/v5.4.4...v6.0.0) (2024-01-10)
9
+
10
+
11
+ ### Features
12
+
13
+ * use config service config ([#487](https://github.com/adobe/helix-html-pipeline/issues/487)) ([f2d9770](https://github.com/adobe/helix-html-pipeline/commit/f2d9770577072691b645c6d30c45358d6606e0a1))
14
+
15
+
16
+ ### BREAKING CHANGES
17
+
18
+ * state needs config
19
+
1
20
  ## [5.4.4](https://github.com/adobe/helix-html-pipeline/compare/v5.4.3...v5.4.4) (2023-12-30)
2
21
 
3
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-html-pipeline",
3
- "version": "5.4.4",
3
+ "version": "6.0.1",
4
4
  "description": "Helix HTML Pipeline",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -75,7 +75,7 @@
75
75
  "@semantic-release/changelog": "6.0.3",
76
76
  "@semantic-release/git": "10.0.1",
77
77
  "@semantic-release/npm": "11.0.2",
78
- "c8": "8.0.1",
78
+ "c8": "9.0.0",
79
79
  "eslint": "8.56.0",
80
80
  "eslint-import-resolver-exports": "1.0.0-beta.5",
81
81
  "eslint-plugin-header": "3.1.1",
@@ -83,7 +83,7 @@
83
83
  "esmock": "2.6.0",
84
84
  "husky": "8.0.3",
85
85
  "js-yaml": "4.1.0",
86
- "jsdom": "23.0.1",
86
+ "jsdom": "23.1.0",
87
87
  "junit-report-builder": "3.1.0",
88
88
  "lint-staged": "15.2.0",
89
89
  "mocha": "10.2.0",
@@ -11,8 +11,7 @@
11
11
  */
12
12
  import {PathInfo, S3Loader, FormsMessageDispatcher, PipelineTimer, AuthEnvLoader } from "./index";
13
13
  import {PipelineContent} from "./PipelineContent";
14
- import {Modifiers} from './utils/modifiers';
15
- import {ProjectConfig} from "./project-config";
14
+ import {PipelineSiteConfig} from "./site-config";
16
15
 
17
16
  declare enum PipelineType {
18
17
  html = 'html',
@@ -37,14 +36,15 @@ declare interface PipelineOptions {
37
36
  s3Loader: S3Loader;
38
37
  messageDispatcher: FormsMessageDispatcher;
39
38
  authEnvLoader: AuthEnvLoader;
39
+ config: PipelineSiteConfig;
40
40
  fetch: Fetch;
41
- owner: string;
42
- repo: string;
43
41
  ref: string;
44
42
  partition: string;
45
43
  path: string;
46
44
  timer: PipelineTimer;
47
45
  env: object;
46
+ site: string;
47
+ org: string;
48
48
  }
49
49
 
50
50
  declare class PipelineState {
@@ -72,6 +72,16 @@ declare class PipelineState {
72
72
  */
73
73
  partition: string;
74
74
 
75
+ /**
76
+ * project site
77
+ */
78
+ site: string;
79
+
80
+ /**
81
+ * project org
82
+ */
83
+ org: string;
84
+
75
85
  /**
76
86
  * Repository owner
77
87
  */
@@ -88,24 +98,19 @@ declare class PipelineState {
88
98
  ref: string;
89
99
 
90
100
  /**
91
- * helix-config.json once loaded (contains fstab, head.html, etc)
92
- */
93
- helixConfig?: object;
94
-
95
- /**
96
- * the /.helix/config.json in object form
101
+ * the site config loaded from config-service
97
102
  */
98
- config?: ProjectConfig;
103
+ config: PipelineSiteConfig;
99
104
 
100
105
  /**
101
106
  * the metadata.json in modifier form.
102
107
  */
103
- metadata?: Modifiers;
108
+ metadata: Modifiers;
104
109
 
105
110
  /**
106
111
  * the headers.json in modifier form.
107
112
  */
108
- headers?: Modifiers;
113
+ headers: Modifiers;
109
114
 
110
115
  /**
111
116
  * optional timer that is used to measure the timing
@@ -122,16 +127,19 @@ declare class PipelineState {
122
127
  */
123
128
  authInfo?: AuthInfo;
124
129
 
130
+ /**
131
+ * the production host
132
+ */
133
+ prodHost: string;
134
+
125
135
  /**
126
136
  * the custom preview host if configured via config.cdn.preview.host
127
- * this is initialized after the config is loaded.
128
137
  */
129
- previewHost?: string;
138
+ previewHost: string;
130
139
 
131
140
  /**
132
141
  * the custom live host if configured via config.cdn.live.host
133
- * this is initialized after the config is loaded.
134
142
  */
135
- liveHost?: string;
143
+ liveHost: string;
136
144
  }
137
145
 
@@ -28,23 +28,29 @@ export class PipelineState {
28
28
  log: opts.log ?? console,
29
29
  env: opts.env,
30
30
  info: getPathInfo(opts.path),
31
+ config: opts.config,
31
32
  content: new PipelineContent(),
32
- owner: opts.owner,
33
- repo: opts.repo,
33
+ contentBusId: opts.config.contentBusId,
34
+ site: opts.site,
35
+ org: opts.org,
36
+ owner: opts.config.owner,
37
+ repo: opts.config.repo,
34
38
  ref: opts.ref,
35
39
  partition: opts.partition,
36
- helixConfig: undefined,
37
40
  metadata: Modifiers.EMPTY,
38
41
  headers: Modifiers.EMPTY,
39
- config: {},
40
42
  s3Loader: opts.s3Loader,
41
43
  messageDispatcher: opts.messageDispatcher,
42
44
  authEnvLoader: opts.authEnvLoader ?? { load: () => {} },
43
45
  fetch: opts.fetch,
44
46
  timer: opts.timer,
45
47
  type: 'html',
46
- authInfo: undefined,
47
48
  });
49
+ for (const prop of ['org', 'site', 'contentBusId', 'repo', 'owner', 'ref', 'partition']) {
50
+ if (!this[prop]) {
51
+ throw new Error(`${prop} required`);
52
+ }
53
+ }
48
54
  }
49
55
 
50
56
  // eslint-disable-next-line class-methods-use-this
package/src/html-pipe.js CHANGED
@@ -10,15 +10,14 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
13
- import { authenticate, requireProject } from './steps/authenticate.js';
13
+ import { authenticate } 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';
17
17
  import extractMetaData from './steps/extract-metadata.js';
18
- import fetchConfig from './steps/fetch-config.js';
19
18
  import fetchContent from './steps/fetch-content.js';
20
19
  import fetch404 from './steps/fetch-404.js';
21
- import fetchConfigAll from './steps/fetch-config-all.js';
20
+ import initConfig from './steps/init-config.js';
22
21
  import fixSections from './steps/fix-sections.js';
23
22
  import folderMapping from './steps/folder-mapping.js';
24
23
  import getMetadata from './steps/get-metadata.js';
@@ -112,14 +111,8 @@ export async function htmlPipe(state, req) {
112
111
  }
113
112
  }
114
113
 
115
- try { // fetch config first, since we need to compute the content-bus-id from the fstab ...
116
- state.timer?.update('config-fetch');
117
- await fetchConfig(state, req, res);
118
- if (!state.contentBusId) {
119
- res.status = 400;
120
- res.headers.set('x-error', 'contentBusId missing');
121
- return res;
122
- }
114
+ try {
115
+ await initConfig(state, req, res);
123
116
 
124
117
  // force code-bus for .html files
125
118
  if (state.info.originalExtension === '.html' && state.info.selector !== 'plain') {
@@ -147,12 +140,11 @@ export async function htmlPipe(state, req) {
147
140
  // load metadata and content in parallel
148
141
  state.timer?.update('metadata-fetch');
149
142
  await Promise.all([
150
- fetchConfigAll(state, req, res),
151
143
  contentPromise,
152
144
  fetchMappedMetadata(state),
153
145
  ]);
154
146
 
155
- await requireProject(state, req, res);
147
+ // await requireProject(state, req, res);
156
148
  if (res.error !== 401) {
157
149
  await authenticate(state, req, res);
158
150
  }
@@ -202,6 +194,7 @@ export async function htmlPipe(state, req) {
202
194
  res.status = 500;
203
195
  }
204
196
 
197
+ /* c8 ignore next */
205
198
  const level = res.status >= 500 ? 'error' : 'info';
206
199
  log[level](`pipeline status: ${res.status} ${res.error}`, e);
207
200
  res.headers.set('x-error', cleanupHeaderValue(res.error));
package/src/index.js CHANGED
@@ -12,7 +12,6 @@
12
12
  export * from './html-pipe.js';
13
13
  export * from './json-pipe.js';
14
14
  export * from './options-pipe.js';
15
- export * from './forms-pipe.js';
16
15
  export * from './PipelineContent.js';
17
16
  export * from './PipelineRequest.js';
18
17
  export * from './PipelineResponse.js';
package/src/json-pipe.js CHANGED
@@ -10,13 +10,12 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  import { cleanupHeaderValue, computeSurrogateKey } from '@adobe/helix-shared-utils';
13
- import fetchConfigAll from './steps/fetch-config-all.js';
13
+ import initConfig from './steps/init-config.js';
14
14
  import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
15
15
  import { PipelineResponse } from './PipelineResponse.js';
16
16
  import jsonFilter from './utils/json-filter.js';
17
17
  import { extractLastModified, updateLastModified } from './utils/last-modified.js';
18
18
  import { authenticate } from './steps/authenticate.js';
19
- import fetchConfig from './steps/fetch-config.js';
20
19
  import { getPathInfo } from './utils/path.js';
21
20
  import { PipelineStatusError } from './PipelineStatusError.js';
22
21
 
@@ -28,7 +27,7 @@ import { PipelineStatusError } from './PipelineStatusError.js';
28
27
  * @param {PipelineState} state
29
28
  */
30
29
  export default function folderMapping(state) {
31
- const folders = state.helixConfig?.fstab?.data.folders;
30
+ const { folders } = state.config;
32
31
  if (!folders) {
33
32
  return;
34
33
  }
@@ -121,39 +120,25 @@ export async function jsonPipe(state, req) {
121
120
  }
122
121
 
123
122
  try {
124
- // fetch config and apply the folder mapping
125
- await fetchConfig(state, req);
126
- if (!state.contentBusId) {
127
- return new PipelineResponse('', {
128
- status: 400,
129
- headers: {
130
- 'x-error': 'contentBusId missing',
131
- },
132
- });
133
- }
134
-
135
123
  /** @type PipelineResponse */
136
124
  const res = new PipelineResponse('', {
137
125
  headers: {
138
126
  'content-type': 'application/json',
139
127
  },
140
128
  });
129
+ await initConfig(state, req, res);
141
130
 
142
131
  // apply the folder mapping if the current resource doesn't exist
143
132
  state.timer?.update('json-fetch');
144
- let contentPromise = await fetchJsonContent(state, req, res);
133
+ await fetchJsonContent(state, req, res);
145
134
  if (res.status === 404) {
146
135
  folderMapping(state);
147
136
  if (state.info.unmappedPath) {
148
- contentPromise = fetchJsonContent(state, req, res);
137
+ await fetchJsonContent(state, req, res);
149
138
  }
150
139
  }
151
140
 
152
141
  state.timer?.update('json-metadata-fetch');
153
- await Promise.all([
154
- fetchConfigAll(state, req, res),
155
- contentPromise,
156
- ]);
157
142
 
158
143
  await authenticate(state, req, res);
159
144
 
@@ -13,9 +13,8 @@ import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
13
13
  // eslint-disable-next-line import/no-unresolved
14
14
  import cryptoImpl from '#crypto';
15
15
  import { PipelineResponse } from './PipelineResponse.js';
16
- import fetchConfigAll from './steps/fetch-config-all.js';
16
+ import initConfig from './steps/init-config.js';
17
17
  import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
18
- import fetchConfig from './steps/fetch-config.js';
19
18
  import { PipelineStatusError } from './PipelineStatusError.js';
20
19
  import { getOriginalHost } from './steps/utils.js';
21
20
 
@@ -41,8 +40,8 @@ function hashMe(domain, domainkeys) {
41
40
  * the x-forwarded-host and the domainkey. If no domainkey has been set in .helix/config
42
41
  * then the `slack` channel will be used instead.
43
42
  * @param {object} state current pipeline state
44
- * @param {Request} request HTTP request
45
- * @param {Response} response HTTP response
43
+ * @param {PipelineRequest} request HTTP request
44
+ * @param {PipelineResponse} response HTTP response
46
45
  * @returns {void}
47
46
  */
48
47
  function setDomainkeyHeader(state, request, response) {
@@ -53,11 +52,11 @@ function setDomainkeyHeader(state, request, response) {
53
52
  // get x-forwarded-host
54
53
  const originalHost = getOriginalHost(request.headers);
55
54
  // get liveHost
56
- const { host } = state.config;
55
+ const { prodHost } = state;
57
56
 
58
- if (originalHost !== host) {
57
+ if (originalHost !== prodHost) {
59
58
  // these have to match
60
- state.log.debug(`x-forwarded-host: ${originalHost} does not match prod host: ${host}`);
59
+ state.log.debug(`x-forwarded-host: ${originalHost} does not match prod host: ${prodHost}`);
61
60
  return;
62
61
  }
63
62
  // get domainkey from config
@@ -83,16 +82,6 @@ function setDomainkeyHeader(state, request, response) {
83
82
  */
84
83
  export async function optionsPipe(state, request) {
85
84
  try {
86
- await fetchConfig(state, request);
87
- if (!state.contentBusId) {
88
- return new PipelineResponse('', {
89
- status: 400,
90
- headers: {
91
- 'x-error': 'contentBusId missing',
92
- },
93
- });
94
- }
95
-
96
85
  // todo: improve
97
86
  const res = new PipelineResponse('', {
98
87
  status: 204,
@@ -103,15 +92,16 @@ export async function optionsPipe(state, request) {
103
92
  'access-control-allow-headers': 'content-type',
104
93
  },
105
94
  });
106
- await fetchConfigAll(state, request, res);
107
- await setCustomResponseHeaders(state, request, res);
95
+ initConfig(state, request, res);
96
+ setCustomResponseHeaders(state, request, res);
108
97
  setDomainkeyHeader(state, request, res);
109
-
110
98
  return res;
111
99
  } catch (e) {
112
100
  const res = new PipelineResponse('', {
101
+ /* c8 ignore next */
113
102
  status: e instanceof PipelineStatusError ? e.code : 500,
114
103
  });
104
+ /* c8 ignore next */
115
105
  const level = res.status >= 500 ? 'error' : 'info';
116
106
  state.log[level](`pipeline status: ${res.status} ${e.message}`);
117
107
  res.headers.set('x-error', cleanupHeaderValue(e.message));
@@ -9,38 +9,34 @@
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 { Modifiers } from './utils/modifiers';
12
13
 
13
- // NOTE: this file is autogenerated via 'npm run docs:types' in helix-admin-support
14
-
15
- export interface ProjectConfig {
16
- /**
17
- * Name of the project used by the slack bot when reporting.
18
- */
19
- name?: string;
20
- /**
21
- * Name of the project used by the sidekick.
22
- */
23
- project?: string;
24
- /**
25
- * Timezone to be used by the slack bot when reporting times.
26
- */
27
- timezone?: string;
28
- /**
29
- * Production host use by the slack bot to display project information.
30
- */
31
- host?: string;
32
- /**
33
- * configuration blueprint repository in the owner/repo format.
34
- */
35
- blueprint?: string;
36
- /**
37
- * the slack teamId/channelID(s) where the slack bot is used.
38
- */
39
- slack?: string | string[];
14
+ /**
15
+ * Resolved site config from config service. Passed via pipeline state
16
+ * @todo: generate from schema in config service
17
+ */
18
+ export interface PipelineSiteConfig {
19
+ contentBusId: string;
20
+ owner: string;
21
+ repo: string;
22
+ ref: string;
23
+ org: string;
24
+ site: string;
25
+ headers: ModifiersSheet;
26
+ metadata: {
27
+ preview: ModifiersSheet;
28
+ live: ModifiersSheet;
29
+ }
30
+ head: {
31
+ html: string;
32
+ }
40
33
  cdn?: ProjectCDNConfig;
41
34
  access?: SiteAccessConfig;
42
- admin?: AdminConfig;
43
35
  }
36
+ export interface ModifiersSheet {
37
+ data: Modifiers;
38
+ }
39
+
44
40
  /**
45
41
  * The CDN config
46
42
  */
@@ -123,35 +119,19 @@ export interface ManagedConfig {
123
119
  route: string | string[];
124
120
  }
125
121
  export interface SiteAccessConfig {
122
+ preview?: AccessConfig;
123
+ live?: AccessConfig;
124
+ }
125
+ /**
126
+ * Access config specific to preview content
127
+ */
128
+ export interface AccessConfig {
126
129
  /**
127
130
  * The email glob of the users that are allowed.
128
131
  */
129
- allow: string | string[];
130
- /**
131
- * the id of the API key(s). this is used to validate the API KEYS and allows to invalidate them.
132
- */
133
- apiKeyId?: string | string[];
134
- require?: {
135
- /**
136
- * The list of owner/repo pointers to projects that are allowed to use this content.
137
- */
138
- repository: string | string[];
139
- };
140
- }
141
- export interface AdminConfig {
142
- role?: Role;
132
+ allow: string[];
143
133
  /**
144
134
  * the id of the API key(s). this is used to validate the API KEYS and allows to invalidate them.
145
135
  */
146
- apiKeyId?: string | string[];
147
- }
148
- export interface Role {
149
- /**
150
- * The email glob of the users with author role.
151
- */
152
- author?: string | string[];
153
- /**
154
- * The email glob of the users with publish role.
155
- */
156
- publish?: string | string[];
136
+ apiKeyId: string[];
157
137
  }
@@ -10,7 +10,6 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  import { getAuthInfo, makeAuthError } from '../utils/auth.js';
13
- import { toArray } from './utils.js';
14
13
 
15
14
  /**
16
15
  * Checks if the given email is allowed.
@@ -28,26 +27,6 @@ export function isAllowed(email = '', allows = []) {
28
27
  return allows.findIndex((a) => a === email || a === wild) >= 0;
29
28
  }
30
29
 
31
- /**
32
- * Returns the normalized access configuration for the current partition.
33
- * @param state
34
- * @return {{}}
35
- */
36
- export function getAccessConfig(state) {
37
- const { access } = state.config;
38
- if (!access) {
39
- return {
40
- allow: [],
41
- apiKeyId: [],
42
- };
43
- }
44
- const { partition } = state;
45
- return {
46
- allow: toArray(access[partition]?.allow ?? access.allow),
47
- apiKeyId: toArray(access[partition]?.apiKeyId ?? access.apiKeyId),
48
- };
49
- }
50
-
51
30
  /**
52
31
  * Handles authentication
53
32
  * @type PipelineStep
@@ -65,7 +44,10 @@ export async function authenticate(state, req, res) {
65
44
  }
66
45
 
67
46
  // get partition relative auth info
68
- const access = getAccessConfig(state);
47
+ const access = state.config.access?.[state.partition] || {
48
+ allow: [],
49
+ apiKeyId: [],
50
+ };
69
51
 
70
52
  // if not protected, do nothing
71
53
  if (!access.allow.length) {
@@ -138,17 +120,17 @@ export function isOwnerRepoAllowed(owner, repo, allows = []) {
138
120
  * @param {PipelineResponse} res
139
121
  * @returns {Promise<void>}
140
122
  */
141
- export async function requireProject(state, req, res) {
142
- // if not restricted, do nothing
143
- const ownerRepo = state.config?.access?.require?.repository;
144
- if (!ownerRepo) {
145
- return;
146
- }
147
- const ownerRepos = Array.isArray(ownerRepo) ? ownerRepo : [ownerRepo];
148
- const { log, owner, repo } = state;
149
- if (!isOwnerRepoAllowed(owner, repo, ownerRepos)) {
150
- log.warn(`${owner}/${repo} not allowed for ${ownerRepos}`);
151
- res.status = 403;
152
- res.error = 'forbidden.';
153
- }
154
- }
123
+ // export async function requireProject(state, req, res) {
124
+ // // if not restricted, do nothing
125
+ // const ownerRepo = state.config?.access?.require?.repository;
126
+ // if (!ownerRepo) {
127
+ // return;
128
+ // }
129
+ // const ownerRepos = Array.isArray(ownerRepo) ? ownerRepo : [ownerRepo];
130
+ // const { log, owner, repo } = state;
131
+ // if (!isOwnerRepoAllowed(owner, repo, ownerRepos)) {
132
+ // log.warn(`${owner}/${repo} not allowed for ${ownerRepos}`);
133
+ // res.status = 403;
134
+ // res.error = 'forbidden.';
135
+ // }
136
+ // }
@@ -40,7 +40,7 @@ export function mapPath(folders, path) {
40
40
  * @param {PipelineState} state
41
41
  */
42
42
  export default function folderMapping(state) {
43
- const folders = state.helixConfig?.fstab?.data.folders;
43
+ const { folders } = state.config;
44
44
  if (!folders) {
45
45
  return;
46
46
  }
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Copyright 2021 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import { Modifiers } from '../utils/modifiers.js';
13
+ import { getOriginalHost } from './utils.js';
14
+ import { updateLastModified } from '../utils/last-modified.js';
15
+
16
+ function replaceParams(str, info) {
17
+ if (!str) {
18
+ return '';
19
+ }
20
+ return str
21
+ .replaceAll('$owner', info.owner)
22
+ .replaceAll('$org', info.org)
23
+ .replaceAll('$site', info.site)
24
+ .replaceAll('$repo', info.repo)
25
+ .replaceAll('$ref', info.ref);
26
+ }
27
+
28
+ /**
29
+ * Initializes the pipeline state with the config from the config service
30
+ * (passed via the `config` parameter during state construction).
31
+ *
32
+ * @type PipelineStep
33
+ * @param {PipelineState} state
34
+ * @param {PipelineRequest} req
35
+ * @param {PipelineResponse} res
36
+ * @returns {Promise<void>}
37
+ */
38
+ export default function initConfig(state, req, res) {
39
+ const { config, partition } = state;
40
+ state.metadata = new Modifiers(config.metadata?.[partition]?.data || {});
41
+ state.headers = new Modifiers(config.headers || {});
42
+
43
+ // set custom preview and live hosts
44
+ state.previewHost = replaceParams(config.cdn?.preview?.host, state);
45
+ state.liveHost = replaceParams(config.cdn?.live?.host, state);
46
+ state.prodHost = config.cdn?.prod?.host || getOriginalHost(req.headers);
47
+ updateLastModified(state, res, state.config.lastModified);
48
+ }
@@ -72,7 +72,7 @@ export default async function render(state, req, res) {
72
72
  appendElement($head, createElement('link', 'rel', 'alternate', 'type', 'application/xml+atom', 'href', meta.feed, 'title', `${meta.title} feed`));
73
73
 
74
74
  // inject head.html
75
- const headHtml = state.helixConfig?.head?.data.html;
75
+ const headHtml = state.config?.head?.html;
76
76
  if (headHtml) {
77
77
  const $headHtml = await unified()
78
78
  .use(rehypeParse, { fragment: true })
@@ -172,7 +172,7 @@ export function getAbsoluteUrl(state, url) {
172
172
  if (typeof url !== 'string') {
173
173
  return null;
174
174
  }
175
- return resolveUrl(`https://${state.config.host}/`, url);
175
+ return resolveUrl(`https://${state.prodHost}/`, url);
176
176
  }
177
177
 
178
178
  /**
package/src/utils/auth.js CHANGED
@@ -99,7 +99,7 @@ export async function decodeIdToken(state, idToken, lenient = false) {
99
99
 
100
100
  /**
101
101
  * Returns the host of the request; falls back to the configured `host`.
102
- * Note that this is different from the `config.host` calculation in `fetch-config-all`,
102
+ * Note that this is different from the `state.prodHost` calculation in `init-config`,
103
103
  * as this prefers the xfh over the config.
104
104
  *
105
105
  * @param {PipelineState} state
@@ -115,7 +115,7 @@ function getRequestHostAndProto(state, req) {
115
115
  host = host.split(',')[0].trim();
116
116
  }
117
117
  if (!host) {
118
- host = state.config.host;
118
+ host = state.prodHost;
119
119
  }
120
120
  // fastly overrides the x-forwarded-proto, so we use x-forwarded-scheme
121
121
  const proto = req.headers.get('x-forwarded-scheme') || req.headers.get('x-forwarded-proto') || 'https';
@@ -239,8 +239,8 @@ export class AuthInfo {
239
239
  const url = new URL(idp.discovery.authorization_endpoint);
240
240
 
241
241
  const tokenState = await signJWT(state, new SignJWT({
242
- owner: state.owner,
243
- repo: state.repo,
242
+ org: state.org,
243
+ site: state.site,
244
244
  // this is our own login redirect, i.e. the current document
245
245
  requestPath: state.info.path,
246
246
  requestHost: host,
@@ -400,8 +400,8 @@ export async function initAuthRoute(state, req, res) {
400
400
  }
401
401
 
402
402
  // fixup pipeline state
403
- state.owner = req.params.state.owner;
404
- state.repo = req.params.state.repo;
403
+ state.org = req.params.state.org;
404
+ state.site = req.params.state.site;
405
405
  state.ref = 'main';
406
406
  state.partition = 'preview';
407
407
  state.info.path = '/.auth';
package/src/forms-pipe.js DELETED
@@ -1,189 +0,0 @@
1
- /*
2
- * Copyright 2022 Adobe. All rights reserved.
3
- * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License. You may obtain a copy
5
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- *
7
- * Unless required by applicable law or agreed to in writing, software distributed under
8
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
- * OF ANY KIND, either express or implied. See the License for the specific language
10
- * governing permissions and limitations under the License.
11
- */
12
- import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
13
- import { PipelineResponse } from './PipelineResponse.js';
14
- import fetchConfigAll from './steps/fetch-config-all.js';
15
- import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
16
- import { authenticate } from './steps/authenticate.js';
17
- import fetchConfig from './steps/fetch-config.js';
18
- import validateCaptcha from './steps/validate-captcha.js';
19
-
20
- function error(log, msg, status, response) {
21
- log.error(msg);
22
- response.status = status;
23
- response.headers.set('x-error', cleanupHeaderValue(msg));
24
- return response;
25
- }
26
-
27
- /**
28
- * Converts URLSearchParams to an object
29
- * @param {URLSearchParams} searchParams the search params object
30
- * @returns {Object} The converted object
31
- */
32
- function searchParamsToObject(searchParams) {
33
- const result = Object.create(null);
34
-
35
- for (const key of searchParams.keys()) {
36
- // get all values association with the key
37
- const values = searchParams.getAll(key);
38
-
39
- // if multiple values, convert to array
40
- result[key] = (values.length === 1) ? values[0] : values;
41
- }
42
-
43
- return result;
44
- }
45
-
46
- /**
47
- * Extracts and parses the body data from the request
48
- * @param {PipelineRequest} request the request object (see fetch api)
49
- * @returns {Object} The body data
50
- * @throws {Error} If an error occurs parsing the body
51
- */
52
- export async function extractBodyData(request) {
53
- let { body } = request;
54
- if (!body) {
55
- throw Error('missing body');
56
- }
57
- const type = request.headers.get('content-type');
58
-
59
- // if content is form-urlencoded we need place the object in the body
60
- // in a "data" property in the body as this is what forms-service expects.
61
- if (/^application\/x-www-form-urlencoded/.test(type)) {
62
- // did they pass an object in the body when a form-urlencoded body was expected?
63
- if (body === '[object Object]') {
64
- throw Error('invalid form-urlencoded body');
65
- }
66
-
67
- body = {
68
- data: searchParamsToObject(new URLSearchParams(body)),
69
- };
70
-
71
- // else treat the body as json
72
- } else if (/^application\/json/.test(type)) {
73
- body = JSON.parse(body);
74
- // verify the body data is as expected
75
- if (!body.data) {
76
- throw Error('missing body.data');
77
- }
78
- } else {
79
- throw Error(`post body content-type not supported: ${type}`);
80
- }
81
- return body;
82
- }
83
-
84
- /**
85
- * Handle a pipeline POST request.
86
- * At this point POST's only apply to json files that are backed by a workbook.
87
- * @param {PipelineState} state pipeline options
88
- * @param {PipelineRequest} req
89
- * @returns {Promise<PipelineResponse>} a response
90
- */
91
- export async function formsPipe(state, req) {
92
- const { log } = state;
93
- state.type = 'form';
94
-
95
- // todo: improve
96
- const res = new PipelineResponse('', {
97
- headers: {
98
- 'content-type': 'text/plain; charset=utf-8',
99
- },
100
- });
101
- try {
102
- await fetchConfig(state, req, res);
103
- } catch (e) {
104
- // ignore
105
- }
106
- if (!state.contentBusId) {
107
- res.status = 400;
108
- res.headers.set('x-error', 'contentBusId missing');
109
- return res;
110
- }
111
-
112
- await fetchConfigAll(state, req, res);
113
- await authenticate(state, req, res);
114
- if (res.error) {
115
- return res;
116
- }
117
- await setCustomResponseHeaders(state, req, res);
118
-
119
- const {
120
- owner, repo, ref, contentBusId, partition, s3Loader,
121
- } = state;
122
- const { path } = state.info;
123
- const resourcePath = `${path}.json`;
124
-
125
- // block all POSTs to resources with extensions
126
- if (state.info.originalExtension !== '') {
127
- return error(log, 'POST to URL with extension not allowed', 405, res);
128
- }
129
-
130
- let body;
131
- try {
132
- body = await extractBodyData(req);
133
- } catch (err) {
134
- return error(log, err.message, 400, res);
135
- }
136
-
137
- // verify captcha if needed
138
- try {
139
- await validateCaptcha(state, body);
140
- } catch (e) {
141
- return error(log, e.message, e.code, res);
142
- }
143
-
144
- // head workbook in content bus
145
- const resourceFetchResponse = await s3Loader.headObject('helix-content-bus', `${contentBusId}/${partition}${resourcePath}`);
146
- if (resourceFetchResponse.status !== 200) {
147
- return resourceFetchResponse;
148
- }
149
-
150
- const sheets = resourceFetchResponse.headers.get('x-amz-meta-x-sheet-names');
151
- if (!sheets) {
152
- return error(log, `Target workbook at ${resourcePath} missing x-sheet-names header.`, 403, res);
153
- }
154
-
155
- const sourceLocation = resourceFetchResponse.headers.get('x-amz-meta-x-source-location');
156
- const referer = req.headers.get('referer') || 'unknown';
157
- const sheetNames = sheets.split(',').map((s) => s.trim());
158
-
159
- if (!sourceLocation || !sheetNames.includes('incoming')) {
160
- return error(log, `Target workbook at ${resourcePath} is not setup to intake data.`, 403, res);
161
- }
162
-
163
- // Send message to SQS if workbook contains and incoming
164
- // sheet and the source location is not null
165
- const { host } = state.config;
166
-
167
- // Forms service expect owner and repo in the message body
168
- body.owner = owner;
169
- body.repo = repo;
170
-
171
- const message = {
172
- url: `https://${ref}--${repo}--${owner}.hlx.live${resourcePath}`,
173
- body,
174
- host,
175
- sourceLocation,
176
- referer,
177
- };
178
-
179
- try {
180
- // Send message to forms queue
181
- const { requestId, messageId } = await state.messageDispatcher.dispatch(message);
182
- res.status = 201;
183
- res.headers.set('x-request-id', requestId);
184
- res.headers.set('x-message-id', messageId);
185
- return res;
186
- } catch (err) {
187
- return error(log, `Failed to send message to forms queue: ${err}`, 500, res);
188
- }
189
- }
@@ -1,95 +0,0 @@
1
- /*
2
- * Copyright 2021 Adobe. All rights reserved.
3
- * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License. You may obtain a copy
5
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- *
7
- * Unless required by applicable law or agreed to in writing, software distributed under
8
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
- * OF ANY KIND, either express or implied. See the License for the specific language
10
- * governing permissions and limitations under the License.
11
- */
12
-
13
- import { PipelineStatusError } from '../PipelineStatusError.js';
14
- import { extractLastModified, updateLastModified } from '../utils/last-modified.js';
15
- import { globToRegExp, Modifiers } from '../utils/modifiers.js';
16
- import { getOriginalHost } from './utils.js';
17
-
18
- /**
19
- * Computes the routes from the given config value.
20
- * @param {string|string[]|undefined} value
21
- * @return {RegExp[]} and array of regexps for route matching
22
- */
23
- export function computeRoutes(value) {
24
- if (!value) {
25
- return [/.*/];
26
- }
27
- // eslint-disable-next-line no-param-reassign
28
- return (Array.isArray(value) ? value : [value]).map((route) => {
29
- if (route.indexOf('*') >= 0) {
30
- return globToRegExp(route);
31
- }
32
- if (route.endsWith('/')) {
33
- return new RegExp(`^${route}.*$`);
34
- }
35
- return new RegExp(`^${route}(/.*)?$`);
36
- });
37
- }
38
-
39
- function replaceParams(str, info) {
40
- if (!str) {
41
- return '';
42
- }
43
- return str
44
- .replaceAll('$owner', info.owner)
45
- .replaceAll('$repo', info.repo)
46
- .replaceAll('$ref', info.ref);
47
- }
48
-
49
- /**
50
- * Loads the /.helix/config-all.json from the content-bus and stores it in the state. if no
51
- * such config exists, it will load the metadata.json as fallback and separate out the
52
- * `state.headers` and `state.metadata`.
53
- *
54
- * @type PipelineStep
55
- * @param {PipelineState} state
56
- * @param {PipelineRequest} req
57
- * @param {PipelineResponse} res
58
- * @returns {Promise<void>}
59
- */
60
- export default async function fetchConfigAll(state, req, res) {
61
- const { contentBusId, partition } = state;
62
- const key = `${contentBusId}/${partition}/.helix/config-all.json`;
63
- const ret = await state.s3Loader.getObject('helix-content-bus', key);
64
- if (ret.status === 200) {
65
- let json;
66
- try {
67
- json = JSON.parse(ret.body);
68
- } catch (e) {
69
- throw new PipelineStatusError(400, `failed parsing of /.helix/config-all.json: ${e.message}`);
70
- }
71
- state.config = json.config?.data || {};
72
- state.metadata = new Modifiers(json.metadata?.data || {});
73
- state.headers = new Modifiers(json.headers?.data || {});
74
-
75
- if (state.type === 'html' && state.info.selector !== 'plain') {
76
- // also update last-modified (only for extensionless html pipeline)
77
- updateLastModified(state, res, extractLastModified(ret.headers));
78
- }
79
- // set custom preview and live hosts
80
- state.previewHost = replaceParams(state.config.cdn?.preview?.host, state);
81
- state.liveHost = replaceParams(state.config.cdn?.live?.host, state);
82
- } else if (ret.status === 404) {
83
- state.config = {};
84
- state.metadata = new Modifiers({});
85
- state.headers = new Modifiers({});
86
- } else {
87
- throw new PipelineStatusError(502, `failed to load /.helix/config-all.json: ${ret.status}`);
88
- }
89
-
90
- // compute host and routes
91
- if (!state.config.host) {
92
- state.config.host = state.config.cdn?.prod?.host || getOriginalHost(req.headers);
93
- }
94
- state.config.routes = computeRoutes(state.config.cdn?.prod?.route);
95
- }
@@ -1,79 +0,0 @@
1
- /*
2
- * Copyright 2021 Adobe. All rights reserved.
3
- * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License. You may obtain a copy
5
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- *
7
- * Unless required by applicable law or agreed to in writing, software distributed under
8
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
- * OF ANY KIND, either express or implied. See the License for the specific language
10
- * governing permissions and limitations under the License.
11
- */
12
- import { extractLastModified, updateLastModified } from '../utils/last-modified.js';
13
- import { PipelineStatusError } from '../PipelineStatusError.js';
14
-
15
- /**
16
- * Fetches the helix-config.json from the code-bus and stores it in `state.helixConfig`
17
- * @type PipelineStep
18
- * @param {PipelineState} state
19
- * @param {PipelineRequest} req
20
- * @param {PipelineResponse} [res]
21
- * @returns {Promise<void>}
22
- */
23
- export default async function fetchConfig(state, req, res) {
24
- const {
25
- log, owner, repo, ref,
26
- } = state;
27
-
28
- const key = `${owner}/${repo}/${ref}/helix-config.json`;
29
- const ret = await state.s3Loader.getObject('helix-code-bus', key);
30
- if (ret.status !== 200) {
31
- throw new PipelineStatusError(ret.status === 404 ? 404 : 502, `unable to load /helix-config.json: ${ret.status}`);
32
- }
33
- let config;
34
- try {
35
- config = JSON.parse(ret.body);
36
- } catch (e) {
37
- log.info('failed to parse helix-config.json', e);
38
- throw new PipelineStatusError(400, `Failed parsing of /helix-config.json: ${e.message}`);
39
- }
40
-
41
- // upgrade to version 2 if needed
42
- if (config.version !== 2) {
43
- Object.keys(config).forEach((name) => {
44
- config[name] = {
45
- data: config[name],
46
- };
47
- });
48
- }
49
-
50
- // set contentbusid from header if missing in config
51
- const cbid = ret.headers.get('x-amz-meta-x-contentbus-id');
52
- if (!config.content && cbid) {
53
- config.content = {
54
- data: {
55
- '/': {
56
- contentBusId: cbid.substring(2),
57
- },
58
- },
59
- };
60
- }
61
- if (!state.contentBusId) {
62
- state.contentBusId = config.content?.data?.['/']?.contentBusId;
63
- }
64
-
65
- if (res) {
66
- // also update last-modified
67
- const configLastModified = extractLastModified(ret.headers);
68
-
69
- // update last modified of fstab
70
- updateLastModified(state, res, config.fstab?.lastModified || configLastModified);
71
-
72
- // for html requests, also consider the HEAD config
73
- if (state.type === 'html' && state.info.selector !== 'plain' && config.head?.lastModified) {
74
- updateLastModified(state, res, config.head.lastModified);
75
- }
76
- }
77
-
78
- state.helixConfig = config;
79
- }