siarashield_workspace 0.0.58 → 0.0.60
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/README.md +10 -10
- package/esm2022/lib/siara-shield-csp.mjs +4 -8
- package/esm2022/lib/siara-shield-log-utils.mjs +2 -3
- package/esm2022/lib/siara-shield-script-utils.mjs +1 -1
- package/esm2022/lib/siara-shield-urls.mjs +8 -0
- package/esm2022/lib/siara-shield.component.mjs +2 -4
- package/esm2022/lib/siara-shield.mjs +2 -4
- package/esm2022/public-api.mjs +2 -1
- package/fesm2022/siarashield_workspace.mjs +21 -24
- package/fesm2022/siarashield_workspace.mjs.map +1 -1
- package/lib/siara-shield-urls.d.ts +7 -0
- package/package.json +1 -1
- package/public-api.d.ts +1 -0
- package/schematics/ng-add/index.js +215 -215
- package/siarashield_workspace-0.0.59.tgz +0 -0
- package/siarashield_workspace-0.0.58.tgz +0 -0
|
@@ -1,215 +1,215 @@
|
|
|
1
|
-
const { chain, noop, Rule } = require('@angular-devkit/schematics');
|
|
2
|
-
const { getWorkspace } = require('@schematics/angular/utility/workspace');
|
|
3
|
-
|
|
4
|
-
function normalizeNewlines(text) {
|
|
5
|
-
return text.replace(/\r\n/g, '\n');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function addCspMetaToIndexHtml({ projectRoot, noncePlaceholder }) {
|
|
9
|
-
/** @type {Rule} */
|
|
10
|
-
return (tree) => {
|
|
11
|
-
const indexHtmlPath = `${projectRoot}/src/index.html`.replace(/\/+/g, '/');
|
|
12
|
-
if (!tree.exists(indexHtmlPath)) {
|
|
13
|
-
return tree;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const raw = tree.read(indexHtmlPath).toString('utf8');
|
|
17
|
-
const text = normalizeNewlines(raw);
|
|
18
|
-
|
|
19
|
-
const hasNonceMeta = /<meta\s+name=["']csp-nonce["']\s+/i.test(text);
|
|
20
|
-
const hasCspMeta = /<meta\s+http-equiv=["']Content-Security-Policy["']\s+/i.test(text);
|
|
21
|
-
if (hasNonceMeta && hasCspMeta) {
|
|
22
|
-
return tree;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const injection =
|
|
26
|
-
` <meta name="csp-nonce" content="${noncePlaceholder}" />\n` +
|
|
27
|
-
` <meta http-equiv="Content-Security-Policy" content="\n` +
|
|
28
|
-
` default-src 'self';\n` +
|
|
29
|
-
` script-src 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com
|
|
30
|
-
` script-src-elem 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com
|
|
31
|
-
` script-src-attr 'none';\n` +
|
|
32
|
-
` connect-src 'self' https://embed.mycybersiara.com
|
|
33
|
-
` style-src 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com https://mycybersiara.com https://fonts.googleapis.com https://cdnjs.cloudflare.com;\n` +
|
|
34
|
-
` style-src-elem 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com https://mycybersiara.com https://fonts.googleapis.com https://cdnjs.cloudflare.com;\n` +
|
|
35
|
-
` style-src-attr 'none';\n` +
|
|
36
|
-
` font-src 'self' https://fonts.gstatic.com https://mycybersiara.com https://cdnjs.cloudflare.com data:;\n` +
|
|
37
|
-
` img-src 'self' data: https://embed.mycybersiara.com https://
|
|
38
|
-
` ">\n`;
|
|
39
|
-
|
|
40
|
-
let updated = text;
|
|
41
|
-
if (!hasNonceMeta && !hasCspMeta) {
|
|
42
|
-
updated = text.replace(/<\/head>/i, `${injection}</head>`);
|
|
43
|
-
} else if (!hasNonceMeta && hasCspMeta) {
|
|
44
|
-
updated = text.replace(
|
|
45
|
-
/(<meta\s+http-equiv=["']Content-Security-Policy["'][^>]*>)/i,
|
|
46
|
-
` <meta name="csp-nonce" content="${noncePlaceholder}" />\n$1`,
|
|
47
|
-
);
|
|
48
|
-
} else if (hasNonceMeta && !hasCspMeta) {
|
|
49
|
-
updated = text.replace(/<\/head>/i, ` <meta http-equiv="Content-Security-Policy" content="\n` +
|
|
50
|
-
` default-src 'self';\n` +
|
|
51
|
-
` script-src 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com
|
|
52
|
-
` script-src-elem 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com
|
|
53
|
-
` script-src-attr 'none';\n` +
|
|
54
|
-
` connect-src 'self' https://embed.mycybersiara.com
|
|
55
|
-
` style-src 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com https://mycybersiara.com https://fonts.googleapis.com https://cdnjs.cloudflare.com;\n` +
|
|
56
|
-
` style-src-elem 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com https://mycybersiara.com https://fonts.googleapis.com https://cdnjs.cloudflare.com;\n` +
|
|
57
|
-
` style-src-attr 'none';\n` +
|
|
58
|
-
` font-src 'self' https://fonts.gstatic.com https://mycybersiara.com https://cdnjs.cloudflare.com data:;\n` +
|
|
59
|
-
` img-src 'self' data: https://embed.mycybersiara.com https://
|
|
60
|
-
` ">\n</head>`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (updated !== text) {
|
|
64
|
-
tree.overwrite(indexHtmlPath, updated.replace(/\n/g, raw.includes('\r\n') ? '\r\n' : '\n'));
|
|
65
|
-
}
|
|
66
|
-
return tree;
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function addAngularCspNonceProviderToMainTs({ projectRoot }) {
|
|
71
|
-
/** @type {Rule} */
|
|
72
|
-
return (tree) => {
|
|
73
|
-
const mainTsPath = `${projectRoot}/src/main.ts`.replace(/\/+/g, '/');
|
|
74
|
-
if (!tree.exists(mainTsPath)) {
|
|
75
|
-
return tree;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const raw = tree.read(mainTsPath).toString('utf8');
|
|
79
|
-
const text = normalizeNewlines(raw);
|
|
80
|
-
|
|
81
|
-
if (/\bCSP_NONCE\b/.test(text) && /provide\s*:\s*CSP_NONCE/.test(text)) {
|
|
82
|
-
return tree;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
let updated = text;
|
|
86
|
-
|
|
87
|
-
const providerSnippet =
|
|
88
|
-
`{\n` +
|
|
89
|
-
` provide: CSP_NONCE,\n` +
|
|
90
|
-
` useFactory: () => document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') ?? null,\n` +
|
|
91
|
-
` }`;
|
|
92
|
-
|
|
93
|
-
let injected = false;
|
|
94
|
-
|
|
95
|
-
// Try to inject into bootstrapApplication(..., { providers: [...] })
|
|
96
|
-
const providersArrayMatch = updated.match(/providers\s*:\s*\[\s*/);
|
|
97
|
-
if (providersArrayMatch) {
|
|
98
|
-
updated = updated.replace(/providers\s*:\s*\[\s*/m, (m) => `${m}\n ${providerSnippet},\n`);
|
|
99
|
-
injected = true;
|
|
100
|
-
} else {
|
|
101
|
-
// If main.ts uses appConfig object, user can add provider manually; we still try a best-effort append.
|
|
102
|
-
// Insert into first bootstrapApplication call's options object.
|
|
103
|
-
const attempted = updated.replace(
|
|
104
|
-
/bootstrapApplication\(([\s\S]*?),\s*\{\s*/m,
|
|
105
|
-
(m) => `${m}\n providers: [\n ${providerSnippet},\n ],\n`,
|
|
106
|
-
);
|
|
107
|
-
if (attempted !== updated) {
|
|
108
|
-
updated = attempted;
|
|
109
|
-
injected = true;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (injected) {
|
|
114
|
-
// Ensure import exists (only if we actually injected CSP_NONCE usage in this file).
|
|
115
|
-
if (!/from\s+['"]@angular\/core['"]/.test(updated) || !/\bCSP_NONCE\b/.test(updated)) {
|
|
116
|
-
// If there's already an @angular/core import, append CSP_NONCE into it when possible.
|
|
117
|
-
const coreImportMatch = updated.match(/import\s*\{([^}]+)\}\s*from\s*['"]@angular\/core['"];\s*/);
|
|
118
|
-
if (coreImportMatch) {
|
|
119
|
-
const inside = coreImportMatch[1];
|
|
120
|
-
if (!inside.split(',').map((s) => s.trim()).includes('CSP_NONCE')) {
|
|
121
|
-
const replacement = coreImportMatch[0].replace(inside, `${inside.trim()}, CSP_NONCE`);
|
|
122
|
-
updated = updated.replace(coreImportMatch[0], replacement);
|
|
123
|
-
}
|
|
124
|
-
} else {
|
|
125
|
-
updated = `import { CSP_NONCE } from '@angular/core';\n` + updated;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (updated !== text) {
|
|
131
|
-
tree.overwrite(mainTsPath, updated.replace(/\n/g, raw.includes('\r\n') ? '\r\n' : '\n'));
|
|
132
|
-
}
|
|
133
|
-
return tree;
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function addAngularCspNonceProviderToAppConfig({ projectRoot }) {
|
|
138
|
-
/** @type {Rule} */
|
|
139
|
-
return (tree) => {
|
|
140
|
-
const appConfigPath = `${projectRoot}/src/app/app.config.ts`.replace(/\/+/g, '/');
|
|
141
|
-
if (!tree.exists(appConfigPath)) {
|
|
142
|
-
return tree;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const raw = tree.read(appConfigPath).toString('utf8');
|
|
146
|
-
const text = normalizeNewlines(raw);
|
|
147
|
-
if (/\bCSP_NONCE\b/.test(text) && /provide\s*:\s*CSP_NONCE/.test(text)) {
|
|
148
|
-
return tree;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Only patch if there is a providers array in appConfig.
|
|
152
|
-
if (!/providers\s*:\s*\[\s*/m.test(text)) {
|
|
153
|
-
return tree;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let updated = text;
|
|
157
|
-
const providerSnippet =
|
|
158
|
-
`{\n` +
|
|
159
|
-
` provide: CSP_NONCE,\n` +
|
|
160
|
-
` useFactory: () => document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') ?? null,\n` +
|
|
161
|
-
` }`;
|
|
162
|
-
|
|
163
|
-
updated = updated.replace(/providers\s*:\s*\[\s*/m, (m) => `${m}\n ${providerSnippet},\n`);
|
|
164
|
-
|
|
165
|
-
// Ensure import exists.
|
|
166
|
-
const coreImportMatch = updated.match(/import\s*\{([^}]+)\}\s*from\s*['"]@angular\/core['"];\s*/);
|
|
167
|
-
if (coreImportMatch) {
|
|
168
|
-
const inside = coreImportMatch[1];
|
|
169
|
-
if (!inside.split(',').map((s) => s.trim()).includes('CSP_NONCE')) {
|
|
170
|
-
const replacement = coreImportMatch[0].replace(inside, `${inside.trim()}, CSP_NONCE`);
|
|
171
|
-
updated = updated.replace(coreImportMatch[0], replacement);
|
|
172
|
-
}
|
|
173
|
-
} else {
|
|
174
|
-
updated = `import { CSP_NONCE } from '@angular/core';\n` + updated;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (updated !== text) {
|
|
178
|
-
tree.overwrite(appConfigPath, updated.replace(/\n/g, raw.includes('\r\n') ? '\r\n' : '\n'));
|
|
179
|
-
}
|
|
180
|
-
return tree;
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function getProjectRoot(workspace, projectName) {
|
|
185
|
-
const project = workspace.projects.get(projectName);
|
|
186
|
-
if (!project) {
|
|
187
|
-
throw new Error(`Project "${projectName}" not found in workspace.`);
|
|
188
|
-
}
|
|
189
|
-
// For application projects, root is typically '' and sourceRoot is 'src'.
|
|
190
|
-
// We want the project root folder containing 'src'.
|
|
191
|
-
return project.root && project.root.length > 0 ? project.root : '';
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function ngAdd(options) {
|
|
195
|
-
return async (tree, context) => {
|
|
196
|
-
const workspace = await getWorkspace(tree);
|
|
197
|
-
const projectName = options && options.project ? options.project : workspace.extensions.defaultProject;
|
|
198
|
-
if (!projectName) {
|
|
199
|
-
context.logger.warn('No project specified and no defaultProject set; skipping ng-add changes.');
|
|
200
|
-
return tree;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const projectRoot = getProjectRoot(workspace, String(projectName));
|
|
204
|
-
const noncePlaceholder = (options && options.noncePlaceholder) || 'REPLACE_WITH_NONCE';
|
|
205
|
-
|
|
206
|
-
return chain([
|
|
207
|
-
addCspMetaToIndexHtml({ projectRoot, noncePlaceholder }),
|
|
208
|
-
addAngularCspNonceProviderToAppConfig({ projectRoot }),
|
|
209
|
-
addAngularCspNonceProviderToMainTs({ projectRoot }),
|
|
210
|
-
])(tree, context);
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
module.exports = { ngAdd };
|
|
215
|
-
|
|
1
|
+
const { chain, noop, Rule } = require('@angular-devkit/schematics');
|
|
2
|
+
const { getWorkspace } = require('@schematics/angular/utility/workspace');
|
|
3
|
+
|
|
4
|
+
function normalizeNewlines(text) {
|
|
5
|
+
return text.replace(/\r\n/g, '\n');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function addCspMetaToIndexHtml({ projectRoot, noncePlaceholder }) {
|
|
9
|
+
/** @type {Rule} */
|
|
10
|
+
return (tree) => {
|
|
11
|
+
const indexHtmlPath = `${projectRoot}/src/index.html`.replace(/\/+/g, '/');
|
|
12
|
+
if (!tree.exists(indexHtmlPath)) {
|
|
13
|
+
return tree;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const raw = tree.read(indexHtmlPath).toString('utf8');
|
|
17
|
+
const text = normalizeNewlines(raw);
|
|
18
|
+
|
|
19
|
+
const hasNonceMeta = /<meta\s+name=["']csp-nonce["']\s+/i.test(text);
|
|
20
|
+
const hasCspMeta = /<meta\s+http-equiv=["']Content-Security-Policy["']\s+/i.test(text);
|
|
21
|
+
if (hasNonceMeta && hasCspMeta) {
|
|
22
|
+
return tree;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const injection =
|
|
26
|
+
` <meta name="csp-nonce" content="${noncePlaceholder}" />\n` +
|
|
27
|
+
` <meta http-equiv="Content-Security-Policy" content="\n` +
|
|
28
|
+
` default-src 'self';\n` +
|
|
29
|
+
` script-src 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com;\n` +
|
|
30
|
+
` script-src-elem 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com;\n` +
|
|
31
|
+
` script-src-attr 'none';\n` +
|
|
32
|
+
` connect-src 'self' https://embed.mycybersiara.com;\n` +
|
|
33
|
+
` style-src 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com https://mycybersiara.com https://fonts.googleapis.com https://cdnjs.cloudflare.com;\n` +
|
|
34
|
+
` style-src-elem 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com https://mycybersiara.com https://fonts.googleapis.com https://cdnjs.cloudflare.com;\n` +
|
|
35
|
+
` style-src-attr 'none';\n` +
|
|
36
|
+
` font-src 'self' https://fonts.gstatic.com https://mycybersiara.com https://cdnjs.cloudflare.com data:;\n` +
|
|
37
|
+
` img-src 'self' data: https://embed.mycybersiara.com https://mycybersiara.com;\n` +
|
|
38
|
+
` ">\n`;
|
|
39
|
+
|
|
40
|
+
let updated = text;
|
|
41
|
+
if (!hasNonceMeta && !hasCspMeta) {
|
|
42
|
+
updated = text.replace(/<\/head>/i, `${injection}</head>`);
|
|
43
|
+
} else if (!hasNonceMeta && hasCspMeta) {
|
|
44
|
+
updated = text.replace(
|
|
45
|
+
/(<meta\s+http-equiv=["']Content-Security-Policy["'][^>]*>)/i,
|
|
46
|
+
` <meta name="csp-nonce" content="${noncePlaceholder}" />\n$1`,
|
|
47
|
+
);
|
|
48
|
+
} else if (hasNonceMeta && !hasCspMeta) {
|
|
49
|
+
updated = text.replace(/<\/head>/i, ` <meta http-equiv="Content-Security-Policy" content="\n` +
|
|
50
|
+
` default-src 'self';\n` +
|
|
51
|
+
` script-src 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com;\n` +
|
|
52
|
+
` script-src-elem 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com;\n` +
|
|
53
|
+
` script-src-attr 'none';\n` +
|
|
54
|
+
` connect-src 'self' https://embed.mycybersiara.com;\n` +
|
|
55
|
+
` style-src 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com https://mycybersiara.com https://fonts.googleapis.com https://cdnjs.cloudflare.com;\n` +
|
|
56
|
+
` style-src-elem 'self' 'nonce-${noncePlaceholder}' https://embed.mycybersiara.com https://mycybersiara.com https://fonts.googleapis.com https://cdnjs.cloudflare.com;\n` +
|
|
57
|
+
` style-src-attr 'none';\n` +
|
|
58
|
+
` font-src 'self' https://fonts.gstatic.com https://mycybersiara.com https://cdnjs.cloudflare.com data:;\n` +
|
|
59
|
+
` img-src 'self' data: https://embed.mycybersiara.com https://mycybersiara.com;\n` +
|
|
60
|
+
` ">\n</head>`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (updated !== text) {
|
|
64
|
+
tree.overwrite(indexHtmlPath, updated.replace(/\n/g, raw.includes('\r\n') ? '\r\n' : '\n'));
|
|
65
|
+
}
|
|
66
|
+
return tree;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addAngularCspNonceProviderToMainTs({ projectRoot }) {
|
|
71
|
+
/** @type {Rule} */
|
|
72
|
+
return (tree) => {
|
|
73
|
+
const mainTsPath = `${projectRoot}/src/main.ts`.replace(/\/+/g, '/');
|
|
74
|
+
if (!tree.exists(mainTsPath)) {
|
|
75
|
+
return tree;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const raw = tree.read(mainTsPath).toString('utf8');
|
|
79
|
+
const text = normalizeNewlines(raw);
|
|
80
|
+
|
|
81
|
+
if (/\bCSP_NONCE\b/.test(text) && /provide\s*:\s*CSP_NONCE/.test(text)) {
|
|
82
|
+
return tree;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let updated = text;
|
|
86
|
+
|
|
87
|
+
const providerSnippet =
|
|
88
|
+
`{\n` +
|
|
89
|
+
` provide: CSP_NONCE,\n` +
|
|
90
|
+
` useFactory: () => document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') ?? null,\n` +
|
|
91
|
+
` }`;
|
|
92
|
+
|
|
93
|
+
let injected = false;
|
|
94
|
+
|
|
95
|
+
// Try to inject into bootstrapApplication(..., { providers: [...] })
|
|
96
|
+
const providersArrayMatch = updated.match(/providers\s*:\s*\[\s*/);
|
|
97
|
+
if (providersArrayMatch) {
|
|
98
|
+
updated = updated.replace(/providers\s*:\s*\[\s*/m, (m) => `${m}\n ${providerSnippet},\n`);
|
|
99
|
+
injected = true;
|
|
100
|
+
} else {
|
|
101
|
+
// If main.ts uses appConfig object, user can add provider manually; we still try a best-effort append.
|
|
102
|
+
// Insert into first bootstrapApplication call's options object.
|
|
103
|
+
const attempted = updated.replace(
|
|
104
|
+
/bootstrapApplication\(([\s\S]*?),\s*\{\s*/m,
|
|
105
|
+
(m) => `${m}\n providers: [\n ${providerSnippet},\n ],\n`,
|
|
106
|
+
);
|
|
107
|
+
if (attempted !== updated) {
|
|
108
|
+
updated = attempted;
|
|
109
|
+
injected = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (injected) {
|
|
114
|
+
// Ensure import exists (only if we actually injected CSP_NONCE usage in this file).
|
|
115
|
+
if (!/from\s+['"]@angular\/core['"]/.test(updated) || !/\bCSP_NONCE\b/.test(updated)) {
|
|
116
|
+
// If there's already an @angular/core import, append CSP_NONCE into it when possible.
|
|
117
|
+
const coreImportMatch = updated.match(/import\s*\{([^}]+)\}\s*from\s*['"]@angular\/core['"];\s*/);
|
|
118
|
+
if (coreImportMatch) {
|
|
119
|
+
const inside = coreImportMatch[1];
|
|
120
|
+
if (!inside.split(',').map((s) => s.trim()).includes('CSP_NONCE')) {
|
|
121
|
+
const replacement = coreImportMatch[0].replace(inside, `${inside.trim()}, CSP_NONCE`);
|
|
122
|
+
updated = updated.replace(coreImportMatch[0], replacement);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
updated = `import { CSP_NONCE } from '@angular/core';\n` + updated;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (updated !== text) {
|
|
131
|
+
tree.overwrite(mainTsPath, updated.replace(/\n/g, raw.includes('\r\n') ? '\r\n' : '\n'));
|
|
132
|
+
}
|
|
133
|
+
return tree;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function addAngularCspNonceProviderToAppConfig({ projectRoot }) {
|
|
138
|
+
/** @type {Rule} */
|
|
139
|
+
return (tree) => {
|
|
140
|
+
const appConfigPath = `${projectRoot}/src/app/app.config.ts`.replace(/\/+/g, '/');
|
|
141
|
+
if (!tree.exists(appConfigPath)) {
|
|
142
|
+
return tree;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const raw = tree.read(appConfigPath).toString('utf8');
|
|
146
|
+
const text = normalizeNewlines(raw);
|
|
147
|
+
if (/\bCSP_NONCE\b/.test(text) && /provide\s*:\s*CSP_NONCE/.test(text)) {
|
|
148
|
+
return tree;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Only patch if there is a providers array in appConfig.
|
|
152
|
+
if (!/providers\s*:\s*\[\s*/m.test(text)) {
|
|
153
|
+
return tree;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let updated = text;
|
|
157
|
+
const providerSnippet =
|
|
158
|
+
`{\n` +
|
|
159
|
+
` provide: CSP_NONCE,\n` +
|
|
160
|
+
` useFactory: () => document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') ?? null,\n` +
|
|
161
|
+
` }`;
|
|
162
|
+
|
|
163
|
+
updated = updated.replace(/providers\s*:\s*\[\s*/m, (m) => `${m}\n ${providerSnippet},\n`);
|
|
164
|
+
|
|
165
|
+
// Ensure import exists.
|
|
166
|
+
const coreImportMatch = updated.match(/import\s*\{([^}]+)\}\s*from\s*['"]@angular\/core['"];\s*/);
|
|
167
|
+
if (coreImportMatch) {
|
|
168
|
+
const inside = coreImportMatch[1];
|
|
169
|
+
if (!inside.split(',').map((s) => s.trim()).includes('CSP_NONCE')) {
|
|
170
|
+
const replacement = coreImportMatch[0].replace(inside, `${inside.trim()}, CSP_NONCE`);
|
|
171
|
+
updated = updated.replace(coreImportMatch[0], replacement);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
updated = `import { CSP_NONCE } from '@angular/core';\n` + updated;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (updated !== text) {
|
|
178
|
+
tree.overwrite(appConfigPath, updated.replace(/\n/g, raw.includes('\r\n') ? '\r\n' : '\n'));
|
|
179
|
+
}
|
|
180
|
+
return tree;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getProjectRoot(workspace, projectName) {
|
|
185
|
+
const project = workspace.projects.get(projectName);
|
|
186
|
+
if (!project) {
|
|
187
|
+
throw new Error(`Project "${projectName}" not found in workspace.`);
|
|
188
|
+
}
|
|
189
|
+
// For application projects, root is typically '' and sourceRoot is 'src'.
|
|
190
|
+
// We want the project root folder containing 'src'.
|
|
191
|
+
return project.root && project.root.length > 0 ? project.root : '';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function ngAdd(options) {
|
|
195
|
+
return async (tree, context) => {
|
|
196
|
+
const workspace = await getWorkspace(tree);
|
|
197
|
+
const projectName = options && options.project ? options.project : workspace.extensions.defaultProject;
|
|
198
|
+
if (!projectName) {
|
|
199
|
+
context.logger.warn('No project specified and no defaultProject set; skipping ng-add changes.');
|
|
200
|
+
return tree;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const projectRoot = getProjectRoot(workspace, String(projectName));
|
|
204
|
+
const noncePlaceholder = (options && options.noncePlaceholder) || 'REPLACE_WITH_NONCE';
|
|
205
|
+
|
|
206
|
+
return chain([
|
|
207
|
+
addCspMetaToIndexHtml({ projectRoot, noncePlaceholder }),
|
|
208
|
+
addAngularCspNonceProviderToAppConfig({ projectRoot }),
|
|
209
|
+
addAngularCspNonceProviderToMainTs({ projectRoot }),
|
|
210
|
+
])(tree, context);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = { ngAdd };
|
|
215
|
+
|
|
Binary file
|
|
Binary file
|