@adobe/helix-config 2.7.1 → 2.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
+ # [2.9.0](https://github.com/adobe/helix-config/compare/v2.8.0...v2.9.0) (2024-04-25)
2
+
3
+
4
+ ### Features
5
+
6
+ * add token support ([d8e25f5](https://github.com/adobe/helix-config/commit/d8e25f58ae8b083f9fa2970d84e5ab6f03c26372)), closes [#42](https://github.com/adobe/helix-config/issues/42)
7
+
8
+ # [2.8.0](https://github.com/adobe/helix-config/compare/v2.7.1...v2.8.0) (2024-04-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * add support for robots.txt ([#57](https://github.com/adobe/helix-config/issues/57)) ([dedfec3](https://github.com/adobe/helix-config/commit/dedfec38c0ff97953b71ea3d71a4547f981eb56d)), closes [#56](https://github.com/adobe/helix-config/issues/56)
14
+
1
15
  ## [2.7.1](https://github.com/adobe/helix-config/compare/v2.7.0...v2.7.1) (2024-04-24)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-config",
3
- "version": "2.7.1",
3
+ "version": "2.9.0",
4
4
  "description": "Helix Config",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -66,6 +66,19 @@ async function fetchConfigAll(ctx, contentBusId, partition) {
66
66
  return res.json();
67
67
  }
68
68
 
69
+ /**
70
+ * Retrieves the robots.txt from the code-bus
71
+ * @param {ConfigContext} ctx the context
72
+ * @param {string} owner
73
+ * @param {string} repo
74
+ * @returns {Promise<string|null>} the robots.txt
75
+ */
76
+ export async function fetchRobotsTxt(ctx, owner, repo) {
77
+ const key = `${owner}/${repo}/main/robots.txt`;
78
+ const res = await ctx.loader.getObject(HELIX_CODE_BUS, key);
79
+ return res.body;
80
+ }
81
+
69
82
  /**
70
83
  * Loads the content from a helix 4 project.
71
84
  * @param {ConfigContext} ctx
@@ -131,6 +144,12 @@ export async function resolveLegacyConfig(ctx, rso, scope) {
131
144
  if (configAllLive?.metadata) {
132
145
  config.metadata.live = configAllLive.metadata;
133
146
  }
147
+ const robots = await fetchRobotsTxt(ctx, rso.org, rso.site);
148
+ if (robots) {
149
+ config.robots = {
150
+ txt: robots,
151
+ };
152
+ }
134
153
  }
135
154
  return config;
136
155
  }
@@ -18,7 +18,7 @@ import {
18
18
  SCOPE_DELIVERY,
19
19
  SCOPE_RAW,
20
20
  } from './ConfigContext.js';
21
- import { resolveLegacyConfig } from './config-legacy.js';
21
+ import { resolveLegacyConfig, fetchRobotsTxt } from './config-legacy.js';
22
22
 
23
23
  /**
24
24
  * @typedef Config
@@ -72,12 +72,26 @@ export function canonicalArrayString(root, partition, prop) {
72
72
  /**
73
73
  * Returns the normalized access configuration for the give partition.
74
74
  */
75
- export function getAccessConfig(access, partition) {
76
- return {
75
+ export function getAccessConfig(config, partition) {
76
+ const { access, tokens = {} } = config;
77
+ const apiKeyId = toArray(access[partition]?.apiKeyId ?? access.apiKeyId);
78
+ const cfg = {
77
79
  allow: toArray(access[partition]?.allow ?? access.allow),
78
- apiKeyId: toArray(access[partition]?.apiKeyId ?? access.apiKeyId),
80
+ apiKeyId,
81
+ tokenHash: apiKeyId.map((id) => tokens[id]?.hash).filter((hash) => !!hash),
79
82
  clientCertDN: toArray(access[partition]?.clientCertDN ?? access.clientCertDN),
80
83
  };
84
+ // if an allow is defined but no apiKeyId, create a fake one so that auth is still
85
+ // enforced. later we can remove the allow and the apiKeyId in favor of the tokenHash
86
+ if (cfg.allow.length && !cfg.apiKeyId.length) {
87
+ cfg.apiKeyId.push('fake');
88
+ }
89
+ // if an apiKeyId is defined but no tokenHash, create a fake one so that auth is still
90
+ // enforced.
91
+ if (cfg.apiKeyId.length && !cfg.tokenHash.length) {
92
+ cfg.tokenHash.push('n/a');
93
+ }
94
+ return cfg;
81
95
  }
82
96
 
83
97
  /**
@@ -167,6 +181,12 @@ async function resolveConfig(ctx, rso, scope) {
167
181
  preview: await loadMetadata(ctx, config, 'preview'),
168
182
  live: await loadMetadata(ctx, config, 'live'),
169
183
  };
184
+ if (!config.robots) {
185
+ const txt = await fetchRobotsTxt(ctx, config.code.owner, config.code.repo);
186
+ if (txt) {
187
+ config.robots = { txt };
188
+ }
189
+ }
170
190
  }
171
191
  if (scope === SCOPE_PIPELINE || scope === SCOPE_DELIVERY) {
172
192
  config.head = await loadHeadHtml(ctx, config, rso.ref);
@@ -205,8 +225,8 @@ export async function getConfigResponse(ctx, opts) {
205
225
  // normalize access config
206
226
  const { admin = {} } = config.access;
207
227
  config.access = {
208
- preview: getAccessConfig(config.access, 'preview'),
209
- live: getAccessConfig(config.access, 'live'),
228
+ preview: getAccessConfig(config, 'preview'),
229
+ live: getAccessConfig(config, 'live'),
210
230
  // access.require.repository ?
211
231
  };
212
232
  if (opts.scope === SCOPE_ADMIN || opts.scope === SCOPE_RAW) {
@@ -235,13 +255,20 @@ export async function getConfigResponse(ctx, opts) {
235
255
  'x-hlx-auth-allow-preview': canonicalArrayString(config.access, 'preview', 'allow'),
236
256
  'x-hlx-auth-apikey-preview': canonicalArrayString(config.access, 'preview', 'apiKeyId'),
237
257
  'x-hlx-auth-clientdn-preview': canonicalArrayString(config.access, 'preview', 'clientCertDN'),
258
+ 'x-hlx-auth-hash-preview': canonicalArrayString(config.access, 'preview', 'tokenHash'),
238
259
  'x-hlx-auth-allow-live': canonicalArrayString(config.access, 'live', 'allow'),
239
260
  'x-hlx-auth-apikey-live': canonicalArrayString(config.access, 'live', 'apiKeyId'),
240
261
  'x-hlx-auth-clientdn-live': canonicalArrayString(config.access, 'live', 'clientCertDN'),
262
+ 'x-hlx-auth-hash-live': canonicalArrayString(config.access, 'live', 'tokenHash'),
241
263
  },
242
264
  });
243
265
  }
244
266
 
267
+ // delete token hashes
268
+ delete config.tokens;
269
+ delete config.access?.preview?.tokenHash;
270
+ delete config.access?.live?.tokenHash;
271
+
245
272
  if (opts.scope === SCOPE_ADMIN) {
246
273
  const adminConfig = {
247
274
  ...rso,
@@ -254,6 +281,7 @@ export async function getConfigResponse(ctx, opts) {
254
281
  },
255
282
  };
256
283
  delete adminConfig.public;
284
+ delete adminConfig.robots;
257
285
  return new PipelineResponse(JSON.stringify(adminConfig, null, 2), {
258
286
  headers: {
259
287
  'content-type': 'application/json',
@@ -281,13 +309,13 @@ export async function getConfigResponse(ctx, opts) {
281
309
  repo: config.code.repo,
282
310
  ...rso,
283
311
  contentBusId: config.content.contentBusId,
284
- access: config.access,
285
312
  headers: config.headers,
286
313
  head: config.head,
287
314
  metadata: config.metadata,
288
315
  cdn: config.cdn,
289
316
  folders: config.folders,
290
317
  public: config.public,
318
+ robots: config.robots,
291
319
  };
292
320
  return new PipelineResponse(JSON.stringify(pipelineConfig, null, 2), {
293
321
  headers: {
@@ -15,15 +15,8 @@
15
15
  "title": "Site Access Config",
16
16
  "type": "object",
17
17
  "properties": {
18
- "allow": {
19
- "description": "The email glob of the users that are allowed.",
20
- "type": "array",
21
- "items": {
22
- "type": "string"
23
- }
24
- },
25
18
  "apiKeyId": {
26
- "description": "IDs of the api keys that are allowed.",
19
+ "description": "IDs of the api keys (tokens) that are allowed.",
27
20
  "type": "array",
28
21
  "items": {
29
22
  "type": "string"
@@ -46,6 +46,12 @@
46
46
  },
47
47
  "metadata": {
48
48
  "$ref": "https://ns.adobe.com/helix/config/metadata-source"
49
+ },
50
+ "robots": {
51
+ "$ref": "https://ns.adobe.com/helix/config/robots"
52
+ },
53
+ "tokens": {
54
+ "$ref": "https://ns.adobe.com/helix/config/tokens"
49
55
  }
50
56
  },
51
57
  "required": [
@@ -0,0 +1,12 @@
1
+ /*
2
+ * Copyright 2024 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
+ module.exports = require('./robots.schema.json');
@@ -0,0 +1,23 @@
1
+ {
2
+ "meta:license": [
3
+ "Copyright 2024 Adobe. All rights reserved.",
4
+ "This file is licensed to you under the Apache License, Version 2.0 (the \"License\");",
5
+ "you may not use this file except in compliance with the License. You may obtain a copy",
6
+ "of the License at http://www.apache.org/licenses/LICENSE-2.0",
7
+ "",
8
+ "Unless required by applicable law or agreed to in writing, software distributed under",
9
+ "the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS",
10
+ "OF ANY KIND, either express or implied. See the License for the specific language",
11
+ "governing permissions and limitations under the License."
12
+ ],
13
+ "$schema": "http://json-schema.org/draft-07/schema#",
14
+ "$id": "https://ns.adobe.com/helix/config/robots",
15
+ "type": "object",
16
+ "title": "robots",
17
+ "properties": {
18
+ "txt": {
19
+ "type": "string"
20
+ }
21
+ },
22
+ "additionalProperties": false
23
+ }
@@ -51,6 +51,12 @@
51
51
  },
52
52
  "metadata": {
53
53
  "$ref": "https://ns.adobe.com/helix/config/metadata-source"
54
+ },
55
+ "robots": {
56
+ "$ref": "https://ns.adobe.com/helix/config/robots"
57
+ },
58
+ "tokens": {
59
+ "$ref": "https://ns.adobe.com/helix/config/tokens"
54
60
  }
55
61
  },
56
62
  "required": [
@@ -0,0 +1,12 @@
1
+ /*
2
+ * Copyright 2024 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
+ module.exports = require('./tokens.schema.json');
@@ -0,0 +1,38 @@
1
+ {
2
+ "meta:license": [
3
+ "Copyright 2024 Adobe. All rights reserved.",
4
+ "This file is licensed to you under the Apache License, Version 2.0 (the \"License\");",
5
+ "you may not use this file except in compliance with the License. You may obtain a copy",
6
+ "of the License at http://www.apache.org/licenses/LICENSE-2.0",
7
+ "",
8
+ "Unless required by applicable law or agreed to in writing, software distributed under",
9
+ "the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS",
10
+ "OF ANY KIND, either express or implied. See the License for the specific language",
11
+ "governing permissions and limitations under the License."
12
+ ],
13
+ "$schema": "http://json-schema.org/draft-07/schema#",
14
+ "$id": "https://ns.adobe.com/helix/config/tokens",
15
+ "title": "tokens",
16
+ "type": "object",
17
+ "patternProperties": {
18
+ "^[a-zA-Z0-9-_=]+$": {
19
+ "type": "object",
20
+ "properties": {
21
+ "id": {
22
+ "type": "string",
23
+ "pattern": "^[a-zA-Z0-9-_=]+$"
24
+ },
25
+ "hash": {
26
+ "type": "string",
27
+ "pattern": "^[a-zA-Z0-9-_=]+$"
28
+ },
29
+ "created": {
30
+ "type": "string",
31
+ "format": "date-time"
32
+ }
33
+ },
34
+ "additionalProperties": false
35
+ }
36
+ },
37
+ "additionalProperties": false
38
+ }
@@ -9,9 +9,11 @@
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
+ /* eslint-disable no-param-reassign */
12
13
  import { HelixStorage } from './storage.js';
13
14
  import { StatusCodeError } from './status-code-error.js';
14
15
  import {
16
+ createToken,
15
17
  jsonGet,
16
18
  jsonPut,
17
19
  updateCodeSource,
@@ -19,26 +21,111 @@ import {
19
21
  } from './utils.js';
20
22
  import { validate } from './config-validator.js';
21
23
 
22
- const FRAGMENTS = {
23
- sites: {
24
- content: 'object',
25
- code: 'object',
26
- folders: 'object',
27
- headers: 'object',
28
- metadata: 'object',
29
- sidekick: 'object',
30
- cdn: 'object',
31
- 'cdn/prod': 'object',
32
- 'cdn/preview': 'object',
33
- 'cdn/live': 'object',
34
- access: 'object',
35
- 'access/admin': 'object',
36
- 'access/preview': 'object',
37
- 'access/live': 'object',
38
- public: 'object',
24
+ const FRAGMENTS_COMMON = {
25
+ content: 'object',
26
+ code: 'object',
27
+ folders: 'object',
28
+ headers: 'object',
29
+ metadata: 'object',
30
+ sidekick: 'object',
31
+ cdn: {
32
+ '.': 'object',
33
+ prod: 'object',
34
+ preview: 'object',
35
+ live: 'object',
36
+ },
37
+ access: {
38
+ '.': 'object',
39
+ admin: 'object',
40
+ preview: 'object',
41
+ live: 'object',
42
+ },
43
+ public: 'object',
44
+ robots: 'object',
45
+ tokens: {
46
+ '.': 'tokens',
47
+ '.param': {
48
+ name: 'id',
49
+ '.': 'token',
50
+ },
39
51
  },
40
52
  };
41
53
 
54
+ const FRAGMENTS = {
55
+ sites: FRAGMENTS_COMMON,
56
+ profiles: FRAGMENTS_COMMON,
57
+ };
58
+
59
+ export function getFragmentInfo(type, relPath) {
60
+ if (!relPath) {
61
+ return null;
62
+ }
63
+ const parts = relPath.split('/');
64
+ let fragment = FRAGMENTS[type];
65
+ const info = {
66
+ relPath,
67
+ };
68
+ for (const part of parts) {
69
+ let next = fragment[part];
70
+ if (!next) {
71
+ next = fragment['.param'];
72
+ if (!next) {
73
+ throw new StatusCodeError(400, 'invalid object path.');
74
+ }
75
+ info[next.name] = part;
76
+ }
77
+ fragment = next;
78
+ }
79
+ if (typeof fragment === 'string') {
80
+ info.type = fragment;
81
+ } else {
82
+ info.type = fragment['.'];
83
+ }
84
+ return info;
85
+ }
86
+
87
+ /**
88
+ * Redact / transform the token config
89
+ * @param token
90
+ */
91
+ function redactToken(token) {
92
+ return {
93
+ id: token.id,
94
+ created: token.created,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Redact / transform the tokens config
100
+ * @param tokens
101
+ */
102
+ function redactTokens(tokens) {
103
+ return Object.values(tokens).map(redactToken);
104
+ }
105
+
106
+ /**
107
+ * redact information from the config
108
+ * @param {object} config
109
+ * @param {object} frag
110
+ */
111
+ function redact(config, frag) {
112
+ if (!config) {
113
+ return config;
114
+ }
115
+ let ret = config;
116
+ if (frag?.type === 'tokens') {
117
+ ret = redactTokens(config);
118
+ }
119
+ if (frag?.type === 'token') {
120
+ ret = redactToken(config);
121
+ }
122
+ if (ret.tokens) {
123
+ // eslint-disable-next-line no-param-reassign
124
+ ret.tokens = redactTokens(ret.tokens);
125
+ }
126
+ return ret;
127
+ }
128
+
42
129
  /**
43
130
  * General purpose config store.
44
131
  */
@@ -85,42 +172,54 @@ export class ConfigStore {
85
172
  return null;
86
173
  }
87
174
  let obj = JSON.parse(buf);
88
- if (relPath) {
89
- const fragment = FRAGMENTS[this.type][relPath];
90
- if (!fragment) {
91
- throw new StatusCodeError(400, 'invalid object path.');
92
- }
93
- obj = jsonGet(obj, relPath);
175
+ const frag = getFragmentInfo(this.type, relPath);
176
+ if (frag) {
177
+ obj = jsonGet(obj, frag.relPath);
94
178
  }
95
- return obj;
179
+ return redact(obj, frag);
96
180
  }
97
181
 
98
182
  async update(ctx, data, relPath = '') {
99
183
  const storage = HelixStorage.fromContext(ctx).configBus();
100
184
  const buf = await storage.get(this.key);
101
185
  const old = buf ? JSON.parse(buf) : null;
102
- if (relPath) {
186
+ const frag = getFragmentInfo(this.type, relPath);
187
+ let config = data;
188
+ let ret = null;
189
+ if (frag) {
103
190
  if (!old) {
104
191
  throw new StatusCodeError(404, 'config not found.');
105
192
  }
106
- const fragment = FRAGMENTS[this.type][relPath];
107
- if (!fragment) {
108
- throw new StatusCodeError(400, 'invalid object path.');
109
- }
110
193
  if (relPath === 'code') {
111
194
  updateCodeSource(ctx, data);
112
195
  } else if (relPath === 'content') {
113
196
  updateContentSource(ctx, data);
197
+ } else if (frag.type === 'token') {
198
+ // don't allow to update individual token
199
+ throw new StatusCodeError(400, 'invalid object path.');
200
+ } else if (frag.type === 'tokens') {
201
+ // create new token
202
+ const token = createToken(this.org);
203
+ data = {
204
+ ...token,
205
+ };
206
+ // don't store token value in the config
207
+ delete data.value;
208
+ relPath = `tokens/${token.id}`;
209
+ frag.type = 'token';
210
+ ret = token;
211
+ // don't expose hash in return value
212
+ delete ret.hash;
114
213
  }
115
- // eslint-disable-next-line no-param-reassign
116
- data = jsonPut(JSON.parse(buf), relPath, data);
214
+ config = jsonPut(old, relPath, data);
117
215
  } else {
118
- updateContentSource(ctx, data.content);
119
- updateCodeSource(ctx, data.code);
216
+ updateContentSource(ctx, config.content);
217
+ updateCodeSource(ctx, config.code);
120
218
  }
121
- await validate(data, this.type);
122
- await storage.put(this.key, JSON.stringify(data), 'application/json');
123
- await this.purge(ctx, old, data);
219
+ await validate(config, this.type);
220
+ await storage.put(this.key, JSON.stringify(config), 'application/json');
221
+ await this.purge(ctx, buf ? JSON.parse(buf) : null, config);
222
+ return ret ?? redact(data, frag);
124
223
  }
125
224
 
126
225
  async remove(ctx, relPath) {
@@ -129,11 +228,8 @@ export class ConfigStore {
129
228
  if (!buf) {
130
229
  throw new StatusCodeError(404, 'config not found.');
131
230
  }
132
- if (relPath) {
133
- const fragment = FRAGMENTS[this.type][relPath];
134
- if (!fragment) {
135
- throw new StatusCodeError(400, 'invalid object path.');
136
- }
231
+ const frag = getFragmentInfo(this.type, relPath);
232
+ if (frag) {
137
233
  const data = jsonPut(JSON.parse(buf), relPath, null);
138
234
  await validate(data, this.type);
139
235
  await storage.put(this.key, JSON.stringify(data), 'application/json');
@@ -26,8 +26,10 @@ import markupSchema from '../schemas/content-source-markup.schema.cjs';
26
26
  import metadataSchema from '../schemas/metadata-source.schema.cjs';
27
27
  import onedriveSchema from '../schemas/content-source-onedrive.schema.cjs';
28
28
  import profileSchema from '../schemas/profile.schema.cjs';
29
+ import robotsSchema from '../schemas/robots.schema.cjs';
29
30
  import sidekickSchema from '../schemas/sidekick.schema.cjs';
30
31
  import siteSchema from '../schemas/site.schema.cjs';
32
+ import tokensSchema from '../schemas/tokens.schema.cjs';
31
33
 
32
34
  const SCHEMAS = [
33
35
  accessSchema,
@@ -43,8 +45,10 @@ const SCHEMAS = [
43
45
  metadataSchema,
44
46
  onedriveSchema,
45
47
  profileSchema,
48
+ robotsSchema,
46
49
  sidekickSchema,
47
50
  siteSchema,
51
+ tokensSchema,
48
52
  ];
49
53
 
50
54
  const SCHEMA_TYPES = {
@@ -111,3 +111,26 @@ export function updateCodeSource(ctx, code) {
111
111
  }
112
112
  return modified;
113
113
  }
114
+
115
+ /**
116
+ * Creates a random token and hashes it with the given key
117
+ * @param {string} key
118
+ * @returns {object} the token
119
+ */
120
+ export function createToken(key) {
121
+ const secret = crypto.randomBytes(32).toString('base64url');
122
+ const value = `hlx_${secret}`;
123
+ const hash = crypto
124
+ .createHmac('sha512', key)
125
+ .update(value, 'utf-8')
126
+ .digest()
127
+ .toString('base64url');
128
+ const id = crypto.createHash('sha256').update(hash).digest().toString('base64url');
129
+ const created = new Date().toISOString();
130
+ return {
131
+ id,
132
+ value,
133
+ hash,
134
+ created,
135
+ };
136
+ }
@@ -31,6 +31,8 @@ export interface HelixProfileConfig {
31
31
  access?: Access;
32
32
  sidekick?: SidekickConfig;
33
33
  metadata?: Metadata;
34
+ robots?: Robots;
35
+ tokens?: Tokens;
34
36
  }
35
37
  /**
36
38
  * Defines the content bus location and source.
@@ -222,11 +224,7 @@ export interface Role {
222
224
  }
223
225
  export interface SiteAccessConfig {
224
226
  /**
225
- * The email glob of the users that are allowed.
226
- */
227
- allow?: string[];
228
- /**
229
- * IDs of the api keys that are allowed.
227
+ * IDs of the api keys (tokens) that are allowed.
230
228
  */
231
229
  apiKeyId?: string[];
232
230
  /**
@@ -247,3 +245,17 @@ export interface SidekickPlugin {
247
245
  export interface Metadata {
248
246
  source?: [] | [string];
249
247
  }
248
+ export interface Robots {
249
+ txt?: string;
250
+ }
251
+ export interface Tokens {
252
+ /**
253
+ * This interface was referenced by `Tokens`'s JSON-Schema definition
254
+ * via the `patternProperty` "^[a-zA-Z0-9-_=]+$".
255
+ */
256
+ [k: string]: {
257
+ id?: string;
258
+ hash?: string;
259
+ created?: string;
260
+ };
261
+ }
@@ -34,6 +34,8 @@ export interface HelixSiteConfig {
34
34
  access?: Access;
35
35
  sidekick?: SidekickConfig;
36
36
  metadata?: Metadata;
37
+ robots?: Robots;
38
+ tokens?: Tokens;
37
39
  }
38
40
  /**
39
41
  * Defines the content bus location and source.
@@ -225,11 +227,7 @@ export interface Role {
225
227
  }
226
228
  export interface SiteAccessConfig {
227
229
  /**
228
- * The email glob of the users that are allowed.
229
- */
230
- allow?: string[];
231
- /**
232
- * IDs of the api keys that are allowed.
230
+ * IDs of the api keys (tokens) that are allowed.
233
231
  */
234
232
  apiKeyId?: string[];
235
233
  /**
@@ -250,3 +248,17 @@ export interface SidekickPlugin {
250
248
  export interface Metadata {
251
249
  source?: [] | [string];
252
250
  }
251
+ export interface Robots {
252
+ txt?: string;
253
+ }
254
+ export interface Tokens {
255
+ /**
256
+ * This interface was referenced by `Tokens`'s JSON-Schema definition
257
+ * via the `patternProperty` "^[a-zA-Z0-9-_=]+$".
258
+ */
259
+ [k: string]: {
260
+ id?: string;
261
+ hash?: string;
262
+ created?: string;
263
+ };
264
+ }