@ctcstack/ctcdocs-core 0.14.0 → 0.16.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/dist/access-classes.d.ts +102 -0
- package/dist/access-classes.js +266 -0
- package/dist/access-configuration.d.ts +33 -0
- package/dist/access-configuration.js +89 -0
- package/dist/access-findings.d.ts +38 -0
- package/dist/access-findings.js +126 -0
- package/dist/corpus-structure.d.ts +39 -0
- package/dist/corpus-structure.js +96 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +5 -1
- package/dist/project-layout.d.ts +27 -0
- package/dist/project-layout.js +37 -1
- package/dist/site-configuration.d.ts +17 -0
- package/dist/site-configuration.js +45 -4
- package/package.json +1 -1
- package/src/access-classes.ts +384 -0
- package/src/access-configuration.ts +139 -0
- package/src/access-findings.ts +183 -0
- package/src/corpus-structure.ts +143 -0
- package/src/index.ts +45 -0
- package/src/project-layout.ts +40 -3
- package/src/site-configuration.ts +80 -4
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface CorpusFolder {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
/** `null` for the Drive root the sync reads. */
|
|
4
|
+
readonly parentId: string | null;
|
|
5
|
+
/** The name in Drive, as typed. */
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/** The name the site shows, without an order number. */
|
|
8
|
+
readonly label: string;
|
|
9
|
+
/** The folder's address, when it has one; the root has none. */
|
|
10
|
+
readonly slug: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface CorpusDocument {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly parentId: string | null;
|
|
15
|
+
readonly slug: string;
|
|
16
|
+
/**
|
|
17
|
+
* The readers the document was last published with (ADR-039). Its class
|
|
18
|
+
* never reaches beyond them: a move that would widen them waits for a rule
|
|
19
|
+
* on its new chain to be confirmed.
|
|
20
|
+
*/
|
|
21
|
+
readonly publishedReaders?: '*' | readonly string[];
|
|
22
|
+
}
|
|
23
|
+
export interface CorpusStructure {
|
|
24
|
+
readonly rootFolderId: string | null;
|
|
25
|
+
readonly folders: ReadonlyMap<string, CorpusFolder>;
|
|
26
|
+
readonly documents: ReadonlyMap<string, CorpusDocument>;
|
|
27
|
+
}
|
|
28
|
+
export declare class CorpusStructureError extends Error {
|
|
29
|
+
readonly name = "CorpusStructureError";
|
|
30
|
+
}
|
|
31
|
+
export declare const EMPTY_CORPUS: CorpusStructure;
|
|
32
|
+
/** Reads the folder chain out of a parsed sync manifest. */
|
|
33
|
+
export declare function parseCorpusStructure(manifest: unknown): CorpusStructure;
|
|
34
|
+
/**
|
|
35
|
+
* The corpus of a project, or an empty one before its first sync. A manifest
|
|
36
|
+
* that exists but cannot be read is an error: guessing an empty corpus there
|
|
37
|
+
* would decide access without the folders it is about.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readCorpusStructure(projectRoot: string): CorpusStructure;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of the synchronized corpus: which folder holds what.
|
|
3
|
+
*
|
|
4
|
+
* Access is decided by a document's chain of Drive folders (ADR-039), and that
|
|
5
|
+
* chain is in the sync manifest. The sync package owns the manifest's full
|
|
6
|
+
* schema; this reads only the fields the chain needs, so that the build and
|
|
7
|
+
* the configuration checks can use it without depending on the sync.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { PROJECT_LAYOUT } from './project-layout.js';
|
|
12
|
+
export class CorpusStructureError extends Error {
|
|
13
|
+
name = 'CorpusStructureError';
|
|
14
|
+
}
|
|
15
|
+
export const EMPTY_CORPUS = Object.freeze({
|
|
16
|
+
rootFolderId: null,
|
|
17
|
+
folders: new Map(),
|
|
18
|
+
documents: new Map(),
|
|
19
|
+
});
|
|
20
|
+
function invalid(path) {
|
|
21
|
+
throw new CorpusStructureError(`${PROJECT_LAYOUT.manifestFile}: ${path} is not what the sync writes.`);
|
|
22
|
+
}
|
|
23
|
+
function entries(value, path) {
|
|
24
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
25
|
+
invalid(path);
|
|
26
|
+
}
|
|
27
|
+
return Object.entries(value);
|
|
28
|
+
}
|
|
29
|
+
function field(source, key, path, nullable = false) {
|
|
30
|
+
const value = source[key];
|
|
31
|
+
if (nullable && value === null) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
35
|
+
invalid(`${path}.${key}`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/** Reads the folder chain out of a parsed sync manifest. */
|
|
40
|
+
export function parseCorpusStructure(manifest) {
|
|
41
|
+
if (typeof manifest !== 'object' || manifest === null) {
|
|
42
|
+
invalid('the manifest');
|
|
43
|
+
}
|
|
44
|
+
const source = manifest;
|
|
45
|
+
const folders = new Map();
|
|
46
|
+
for (const [id, raw] of entries(source.folders, 'folders')) {
|
|
47
|
+
const path = `folders.${id}`;
|
|
48
|
+
const slug = raw.stableSlug;
|
|
49
|
+
folders.set(id, {
|
|
50
|
+
id,
|
|
51
|
+
parentId: field(raw, 'googleParentId', path, true),
|
|
52
|
+
name: field(raw, 'googleName', path) ?? '',
|
|
53
|
+
label: field(raw, 'displayLabel', path) ?? '',
|
|
54
|
+
slug: typeof slug === 'string' && slug.length > 0 ? slug : undefined,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const documents = new Map();
|
|
58
|
+
for (const [id, raw] of entries(source.documents, 'documents')) {
|
|
59
|
+
const path = `documents.${id}`;
|
|
60
|
+
const published = raw.publishedReaders;
|
|
61
|
+
documents.set(id, {
|
|
62
|
+
id,
|
|
63
|
+
parentId: field(raw, 'googleParentId', path, true),
|
|
64
|
+
slug: field(raw, 'stableSlug', path) ?? '',
|
|
65
|
+
...(published === '*' ||
|
|
66
|
+
(Array.isArray(published) &&
|
|
67
|
+
published.every((group) => typeof group === 'string'))
|
|
68
|
+
? { publishedReaders: published }
|
|
69
|
+
: {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const root = source.rootFolderId;
|
|
73
|
+
return {
|
|
74
|
+
rootFolderId: typeof root === 'string' && root.length > 0 ? root : null,
|
|
75
|
+
folders,
|
|
76
|
+
documents,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The corpus of a project, or an empty one before its first sync. A manifest
|
|
81
|
+
* that exists but cannot be read is an error: guessing an empty corpus there
|
|
82
|
+
* would decide access without the folders it is about.
|
|
83
|
+
*/
|
|
84
|
+
export function readCorpusStructure(projectRoot) {
|
|
85
|
+
let text;
|
|
86
|
+
try {
|
|
87
|
+
text = readFileSync(resolve(projectRoot, PROJECT_LAYOUT.manifestFile), 'utf8');
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error.code === 'ENOENT') {
|
|
91
|
+
return EMPTY_CORPUS;
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
return parseCorpusStructure(JSON.parse(text));
|
|
96
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
export { GENERATED_DIRECTORY_ALLOWLIST, GENERATED_FILE_ALLOWLIST, PLATFORM_ROUTE_HREFS, PLATFORM_ROUTES, permanentLinkPath, PROJECT_LAYOUT, RESERVED_SLUGS, SHORT_ID_PATTERN, } from './project-layout.js';
|
|
1
|
+
export { ACCESS_CLASSES_ROUTE, GENERATED_DIRECTORY_ALLOWLIST, GENERATED_FILE_ALLOWLIST, PLATFORM_ROUTE_HREFS, PLATFORM_ROUTES, PLATFORM_WORKERS, permanentLinkPath, PROJECT_LAYOUT, RESERVED_SLUGS, SHORT_ID_PATTERN, } from './project-layout.js';
|
|
2
2
|
export { assertGeneratedPathAllowed, isGeneratedPathAllowed, normalizeRepositoryPath, } from './generated-paths.js';
|
|
3
3
|
export { generatedMarkdownHeader, generatedSourceHeader, } from './ownership-markers.js';
|
|
4
4
|
export { findProjectRoot, ProjectRootError } from './project-root.js';
|
|
5
|
+
export { EVERY_MEMBER, parseAccessConfiguration, } from './access-configuration.js';
|
|
6
|
+
export type { AccessConfiguration, AccessRule, } from './access-configuration.js';
|
|
7
|
+
export { CorpusStructureError, EMPTY_CORPUS, parseCorpusStructure, readCorpusStructure, } from './corpus-structure.js';
|
|
8
|
+
export type { CorpusDocument, CorpusFolder, CorpusStructure, } from './corpus-structure.js';
|
|
9
|
+
export { ADMINS_CLASS, chainReaders, chainRules, classIdentifiers, computeAccessModel, documentClass, folderChain, folderClass, intersectReaders, MEMBERS_CLASS, nextPublishedReaders, widensReaders, } from './access-classes.js';
|
|
10
|
+
export type { AccessClass, AccessModel, ChainReaders, PublishedReaders, Readers, } from './access-classes.js';
|
|
11
|
+
export { accessFindings } from './access-findings.js';
|
|
12
|
+
export type { AccessFinding } from './access-findings.js';
|
|
5
13
|
export { loadSiteConfiguration, parseSiteConfiguration, SiteConfigurationError, } from './site-configuration.js';
|
|
6
|
-
export type { AddressPolicy, BrandConfiguration, DeploymentConfiguration, DeploymentEnvironmentConfiguration, DeploymentEnvironmentConfigurations, DeploymentVisibility, HomeConfiguration, NavigationConfiguration, SiteConfiguration, SyncConfigurationDefaults, } from './site-configuration.js';
|
|
14
|
+
export type { AddressPolicy, BrandConfiguration, DeploymentConfiguration, DeploymentEnvironmentConfiguration, DeploymentEnvironmentConfigurations, DeploymentVisibility, HomeConfiguration, NavigationConfiguration, SignInConfiguration, SiteConfiguration, SyncConfigurationDefaults, } from './site-configuration.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export { GENERATED_DIRECTORY_ALLOWLIST, GENERATED_FILE_ALLOWLIST, PLATFORM_ROUTE_HREFS, PLATFORM_ROUTES, permanentLinkPath, PROJECT_LAYOUT, RESERVED_SLUGS, SHORT_ID_PATTERN, } from './project-layout.js';
|
|
1
|
+
export { ACCESS_CLASSES_ROUTE, GENERATED_DIRECTORY_ALLOWLIST, GENERATED_FILE_ALLOWLIST, PLATFORM_ROUTE_HREFS, PLATFORM_ROUTES, PLATFORM_WORKERS, permanentLinkPath, PROJECT_LAYOUT, RESERVED_SLUGS, SHORT_ID_PATTERN, } from './project-layout.js';
|
|
2
2
|
export { assertGeneratedPathAllowed, isGeneratedPathAllowed, normalizeRepositoryPath, } from './generated-paths.js';
|
|
3
3
|
export { generatedMarkdownHeader, generatedSourceHeader, } from './ownership-markers.js';
|
|
4
4
|
export { findProjectRoot, ProjectRootError } from './project-root.js';
|
|
5
|
+
export { EVERY_MEMBER, parseAccessConfiguration, } from './access-configuration.js';
|
|
6
|
+
export { CorpusStructureError, EMPTY_CORPUS, parseCorpusStructure, readCorpusStructure, } from './corpus-structure.js';
|
|
7
|
+
export { ADMINS_CLASS, chainReaders, chainRules, classIdentifiers, computeAccessModel, documentClass, folderChain, folderClass, intersectReaders, MEMBERS_CLASS, nextPublishedReaders, widensReaders, } from './access-classes.js';
|
|
8
|
+
export { accessFindings } from './access-findings.js';
|
|
5
9
|
export { loadSiteConfiguration, parseSiteConfiguration, SiteConfigurationError, } from './site-configuration.js';
|
package/dist/project-layout.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export declare const PROJECT_LAYOUT: {
|
|
|
15
15
|
readonly configurationFile: "site.config.json";
|
|
16
16
|
/** Deployment target. Wrangler reads it itself, so it cannot be generated. */
|
|
17
17
|
readonly wranglerConfigurationFile: "wrangler.jsonc";
|
|
18
|
+
/**
|
|
19
|
+
* The scheduled Worker that keeps the directory snapshot of a private
|
|
20
|
+
* deployment (ADR-040), deployed beside the site's own Worker.
|
|
21
|
+
*/
|
|
22
|
+
readonly directoryWranglerConfigurationFile: "wrangler.directory.jsonc";
|
|
18
23
|
/** Secret-scanning configuration, whose exemptions `validate` checks. */
|
|
19
24
|
readonly gitleaksConfigurationFile: ".gitleaks.toml";
|
|
20
25
|
/** Served verbatim by the Worker. */
|
|
@@ -32,6 +37,11 @@ export declare const PROJECT_LAYOUT: {
|
|
|
32
37
|
readonly syncReportFile: "data/latest-sync-report.json";
|
|
33
38
|
/** What each document's opening says about its title (ADR-023). */
|
|
34
39
|
readonly titleReportFile: "data/title-report.json";
|
|
40
|
+
/**
|
|
41
|
+
* Which access class every built file belongs to (ADR-039). Written by the
|
|
42
|
+
* build outside `dist`, so it is never published, and read by the Worker.
|
|
43
|
+
*/
|
|
44
|
+
readonly accessMapFile: ".ctcdocs/access-map.json";
|
|
35
45
|
};
|
|
36
46
|
/**
|
|
37
47
|
* Addresses the platform serves itself.
|
|
@@ -59,6 +69,23 @@ export declare const PLATFORM_ROUTES: {
|
|
|
59
69
|
*/
|
|
60
70
|
readonly contentHealth: "content-health";
|
|
61
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* The platform's Workers, as a project's Wrangler configuration names them
|
|
74
|
+
* (ADR-038, ADR-040), and the alias under which they import the build's
|
|
75
|
+
* access map.
|
|
76
|
+
*/
|
|
77
|
+
export declare const PLATFORM_WORKERS: {
|
|
78
|
+
readonly gate: "node_modules/@ctcstack/ctcdocs/worker/index.ts";
|
|
79
|
+
readonly directory: "node_modules/@ctcstack/ctcdocs/worker/directory/index.ts";
|
|
80
|
+
readonly accessMapAlias: "ctcdocs-access-map";
|
|
81
|
+
readonly stateBinding: "KB_STATE";
|
|
82
|
+
readonly directorySchedule: "*/10 * * * *";
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Where the Worker tells a signed-in reader which search bundles, beyond the
|
|
86
|
+
* members bundle, they may open (ADR-039). Answered `{ "bundles": [...] }`.
|
|
87
|
+
*/
|
|
88
|
+
export declare const ACCESS_CLASSES_ROUTE = "/_kb/classes";
|
|
62
89
|
/** Each platform route as the href a link uses. */
|
|
63
90
|
export declare const PLATFORM_ROUTE_HREFS: {
|
|
64
91
|
readonly fullIndex: "/documents/";
|
package/dist/project-layout.js
CHANGED
|
@@ -15,6 +15,11 @@ export const PROJECT_LAYOUT = {
|
|
|
15
15
|
configurationFile: 'site.config.json',
|
|
16
16
|
/** Deployment target. Wrangler reads it itself, so it cannot be generated. */
|
|
17
17
|
wranglerConfigurationFile: 'wrangler.jsonc',
|
|
18
|
+
/**
|
|
19
|
+
* The scheduled Worker that keeps the directory snapshot of a private
|
|
20
|
+
* deployment (ADR-040), deployed beside the site's own Worker.
|
|
21
|
+
*/
|
|
22
|
+
directoryWranglerConfigurationFile: 'wrangler.directory.jsonc',
|
|
18
23
|
/** Secret-scanning configuration, whose exemptions `validate` checks. */
|
|
19
24
|
gitleaksConfigurationFile: '.gitleaks.toml',
|
|
20
25
|
/** Served verbatim by the Worker. */
|
|
@@ -32,6 +37,11 @@ export const PROJECT_LAYOUT = {
|
|
|
32
37
|
syncReportFile: 'data/latest-sync-report.json',
|
|
33
38
|
/** What each document's opening says about its title (ADR-023). */
|
|
34
39
|
titleReportFile: 'data/title-report.json',
|
|
40
|
+
/**
|
|
41
|
+
* Which access class every built file belongs to (ADR-039). Written by the
|
|
42
|
+
* build outside `dist`, so it is never published, and read by the Worker.
|
|
43
|
+
*/
|
|
44
|
+
accessMapFile: '.ctcdocs/access-map.json',
|
|
35
45
|
};
|
|
36
46
|
/**
|
|
37
47
|
* Addresses the platform serves itself.
|
|
@@ -59,12 +69,38 @@ export const PLATFORM_ROUTES = {
|
|
|
59
69
|
*/
|
|
60
70
|
contentHealth: 'content-health',
|
|
61
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* The platform's Workers, as a project's Wrangler configuration names them
|
|
74
|
+
* (ADR-038, ADR-040), and the alias under which they import the build's
|
|
75
|
+
* access map.
|
|
76
|
+
*/
|
|
77
|
+
export const PLATFORM_WORKERS = {
|
|
78
|
+
gate: 'node_modules/@ctcstack/ctcdocs/worker/index.ts',
|
|
79
|
+
directory: 'node_modules/@ctcstack/ctcdocs/worker/directory/index.ts',
|
|
80
|
+
accessMapAlias: 'ctcdocs-access-map',
|
|
81
|
+
stateBinding: 'KB_STATE',
|
|
82
|
+
directorySchedule: '*/10 * * * *',
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Where the Worker tells a signed-in reader which search bundles, beyond the
|
|
86
|
+
* members bundle, they may open (ADR-039). Answered `{ "bundles": [...] }`.
|
|
87
|
+
*/
|
|
88
|
+
export const ACCESS_CLASSES_ROUTE = '/_kb/classes';
|
|
62
89
|
/** Each platform route as the href a link uses. */
|
|
63
90
|
export const PLATFORM_ROUTE_HREFS = {
|
|
64
91
|
fullIndex: `/${PLATFORM_ROUTES.fullIndex}/`,
|
|
65
92
|
contentHealth: `/${PLATFORM_ROUTES.contentHealth}/`,
|
|
66
93
|
};
|
|
67
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Top-level addresses the platform serves beside the corpus: the Worker's
|
|
96
|
+
* sign-in routes (ADR-038), the search bundles, `/pagefind/` and
|
|
97
|
+
* `/pagefind-<class>/` (ADR-039), and the original files under `/assets/`.
|
|
98
|
+
*/
|
|
99
|
+
const SERVED_ADDRESSES = ['auth', 'pagefind', 'assets'];
|
|
100
|
+
export const RESERVED_SLUGS = Object.freeze([
|
|
101
|
+
...Object.values(PLATFORM_ROUTES),
|
|
102
|
+
...SERVED_ADDRESSES,
|
|
103
|
+
]);
|
|
68
104
|
/**
|
|
69
105
|
* A short ID: lowercase hexadecimal, six characters unless six collided with
|
|
70
106
|
* another item's, in which case it is longer. It is recorded once per item and
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AccessConfiguration } from './access-configuration.js';
|
|
1
2
|
export interface BrandConfiguration {
|
|
2
3
|
/** The organization or product the documentation belongs to. */
|
|
3
4
|
readonly name: string;
|
|
@@ -125,7 +126,23 @@ export interface NavigationConfiguration {
|
|
|
125
126
|
readonly addresses: AddressPolicy;
|
|
126
127
|
}
|
|
127
128
|
export type AddressPolicy = 'stable' | 'follow-names';
|
|
129
|
+
/**
|
|
130
|
+
* How a private deployment signs readers in (ADR-038): with Google, admitting
|
|
131
|
+
* only accounts whose `hd` is one of these Workspace domains. An organization
|
|
132
|
+
* with secondary domains lists each, since `hd` names the domain of a
|
|
133
|
+
* reader's own primary address.
|
|
134
|
+
*/
|
|
135
|
+
export interface SignInConfiguration {
|
|
136
|
+
readonly workspaceDomains: readonly string[];
|
|
137
|
+
}
|
|
128
138
|
export interface SiteConfiguration {
|
|
139
|
+
/** Required to serve a private environment through the platform's Worker. */
|
|
140
|
+
readonly signIn?: SignInConfiguration;
|
|
141
|
+
/**
|
|
142
|
+
* Who may read which folder (ADR-039). Absent, every document is open to
|
|
143
|
+
* every reader the deployment admits, as before access rules existed.
|
|
144
|
+
*/
|
|
145
|
+
readonly access?: AccessConfiguration;
|
|
129
146
|
readonly brand: BrandConfiguration;
|
|
130
147
|
readonly deployment: DeploymentConfiguration;
|
|
131
148
|
readonly home: HomeConfiguration;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { readFileSync } from 'node:fs';
|
|
15
15
|
import { resolve } from 'node:path';
|
|
16
|
+
import { parseAccessConfiguration, } from './access-configuration.js';
|
|
16
17
|
import { PROJECT_LAYOUT } from './project-layout.js';
|
|
17
18
|
export class SiteConfigurationError extends Error {
|
|
18
19
|
name = 'SiteConfigurationError';
|
|
@@ -227,12 +228,52 @@ export function parseSiteConfiguration(input) {
|
|
|
227
228
|
}
|
|
228
229
|
const navigationSource = record(root.navigation, 'navigation');
|
|
229
230
|
const homeSource = record(root.home, 'home');
|
|
231
|
+
const deployment = {
|
|
232
|
+
environments: environments(deploymentSource),
|
|
233
|
+
workerName,
|
|
234
|
+
};
|
|
235
|
+
/*
|
|
236
|
+
* Rules close folders to some readers, which a public environment cannot
|
|
237
|
+
* do: everyone may read it. Accepting rules there would promise a boundary
|
|
238
|
+
* that does not exist.
|
|
239
|
+
*/
|
|
240
|
+
let access;
|
|
241
|
+
if (root.access !== undefined) {
|
|
242
|
+
const publicEnvironment = Object.entries(deployment.environments).find(([, environment]) => environment.visibility === 'public');
|
|
243
|
+
if (publicEnvironment) {
|
|
244
|
+
fail('access', `must not be set while the ${publicEnvironment[0]} environment is public`);
|
|
245
|
+
}
|
|
246
|
+
access = parseAccessConfiguration(root.access, fail);
|
|
247
|
+
}
|
|
248
|
+
let signIn;
|
|
249
|
+
if (root.signIn !== undefined) {
|
|
250
|
+
const source = record(root.signIn, 'signIn');
|
|
251
|
+
for (const key of Object.keys(source)) {
|
|
252
|
+
if (key !== 'workspaceDomains') {
|
|
253
|
+
fail(`signIn.${key}`, 'is not a known setting');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const domains = source.workspaceDomains;
|
|
257
|
+
if (!Array.isArray(domains) || domains.length === 0) {
|
|
258
|
+
fail('signIn.workspaceDomains', 'must be a non-empty array of domains');
|
|
259
|
+
}
|
|
260
|
+
const parsed = domains.map((domain, index) => {
|
|
261
|
+
const value = typeof domain === 'string' ? domain.trim().toLowerCase() : '';
|
|
262
|
+
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/u.test(value)) {
|
|
263
|
+
fail(`signIn.workspaceDomains[${index}]`, 'must be a domain such as "example.com"');
|
|
264
|
+
}
|
|
265
|
+
return value;
|
|
266
|
+
});
|
|
267
|
+
if (new Set(parsed).size !== parsed.length) {
|
|
268
|
+
fail('signIn.workspaceDomains', 'must not repeat a domain');
|
|
269
|
+
}
|
|
270
|
+
signIn = { workspaceDomains: Object.freeze(parsed) };
|
|
271
|
+
}
|
|
230
272
|
return {
|
|
273
|
+
...(signIn ? { signIn } : {}),
|
|
274
|
+
...(access ? { access } : {}),
|
|
231
275
|
brand,
|
|
232
|
-
deployment
|
|
233
|
-
environments: environments(deploymentSource),
|
|
234
|
-
workerName,
|
|
235
|
-
},
|
|
276
|
+
deployment,
|
|
236
277
|
home: {
|
|
237
278
|
corpusIndex: optionalFlag(homeSource, 'corpusIndex', 'home.corpusIndex', true),
|
|
238
279
|
lede: text(homeSource, 'lede', 'home.lede'),
|
package/package.json
CHANGED