netlify-cli 15.7.0 → 15.8.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "15.7.0",
4
+ "version": "15.8.1",
5
5
  "author": "Netlify Inc.",
6
6
  "type": "module",
7
7
  "engines": {
@@ -44,14 +44,14 @@
44
44
  "dependencies": {
45
45
  "@bugsnag/js": "7.20.2",
46
46
  "@fastify/static": "6.10.2",
47
- "@netlify/build": "29.12.8",
48
- "@netlify/build-info": "7.0.8",
49
- "@netlify/config": "20.5.1",
47
+ "@netlify/build": "29.15.2",
48
+ "@netlify/build-info": "7.3.4",
49
+ "@netlify/config": "20.5.2",
50
50
  "@netlify/edge-bundler": "8.16.2",
51
51
  "@netlify/framework-info": "9.8.10",
52
52
  "@netlify/local-functions-proxy": "1.1.1",
53
53
  "@netlify/serverless-functions-api": "1.5.1",
54
- "@netlify/zip-it-and-ship-it": "9.10.0",
54
+ "@netlify/zip-it-and-ship-it": "9.12.1",
55
55
  "@octokit/rest": "19.0.13",
56
56
  "@skn0tt/lambda-local": "2.0.3",
57
57
  "ansi-escapes": "6.2.0",
@@ -60,7 +60,7 @@
60
60
  "ascii-table": "0.0.9",
61
61
  "backoff": "2.5.0",
62
62
  "better-opn": "3.0.2",
63
- "boxen": "7.1.0",
63
+ "boxen": "7.1.1",
64
64
  "chalk": "5.2.0",
65
65
  "chokidar": "3.5.3",
66
66
  "ci-info": "3.8.0",
@@ -92,7 +92,7 @@
92
92
  "from2-array": "0.0.4",
93
93
  "fuzzy": "0.1.3",
94
94
  "get-port": "5.1.1",
95
- "gh-release-fetch": "4.0.2",
95
+ "gh-release-fetch": "4.0.3",
96
96
  "git-repo-info": "2.1.1",
97
97
  "gitconfiglocal": "2.1.0",
98
98
  "hasbin": "1.2.3",
@@ -106,7 +106,7 @@
106
106
  "is-stream": "3.0.0",
107
107
  "is-wsl": "2.2.0",
108
108
  "isexe": "2.0.0",
109
- "jsonwebtoken": "9.0.0",
109
+ "jsonwebtoken": "9.0.1",
110
110
  "jwt-decode": "3.1.2",
111
111
  "listr": "0.14.3",
112
112
  "locate-path": "7.2.0",
@@ -119,7 +119,7 @@
119
119
  "netlify-headers-parser": "7.1.2",
120
120
  "netlify-redirect-parser": "14.1.3",
121
121
  "netlify-redirector": "0.4.0",
122
- "node-fetch": "2.6.11",
122
+ "node-fetch": "2.6.12",
123
123
  "node-version-alias": "3.4.1",
124
124
  "ora": "6.3.1",
125
125
  "p-filter": "3.0.0",
@@ -407,7 +407,8 @@ export default class BaseCommand extends Command {
407
407
  }
408
408
 
409
409
  setAnalyticsPayload(payload) {
410
- this.analytics = { ...this.analytics, payload }
410
+ const newPayload = { ...this.analytics.payload, ...payload }
411
+ this.analytics = { ...this.analytics, payload: newPayload }
411
412
  }
412
413
 
413
414
  /**
@@ -18,7 +18,7 @@ import {
18
18
  * @returns {Promise<boolean>}
19
19
  */
20
20
  const envSet = async (key, value, options, command) => {
21
- const { context, scope } = options
21
+ const { context, scope, secret } = options
22
22
 
23
23
  const { api, cachedConfig, site } = command.netlify
24
24
  const siteId = site.id
@@ -33,7 +33,7 @@ const envSet = async (key, value, options, command) => {
33
33
 
34
34
  // Get current environment variables set in the UI
35
35
  if (siteInfo.use_envelope) {
36
- finalEnv = await setInEnvelope({ api, siteInfo, key, value, context, scope })
36
+ finalEnv = await setInEnvelope({ api, siteInfo, key, value, context, scope, secret })
37
37
  } else if (context || scope) {
38
38
  error(
39
39
  `To specify a context or scope, please run ${chalk.yellow(
@@ -56,11 +56,12 @@ const envSet = async (key, value, options, command) => {
56
56
  }
57
57
 
58
58
  const withScope = scope ? ` scoped to ${chalk.white(scope)}` : ''
59
+ const withSecret = secret ? ` as a ${chalk.blue('secret')}` : ''
59
60
  const contextType = AVAILABLE_CONTEXTS.includes(context || 'all') ? 'context' : 'branch'
60
61
  log(
61
- `Set environment variable ${chalk.yellow(`${key}${value ? '=' : ''}${value}`)}${withScope} in the ${chalk.magenta(
62
- context || 'all',
63
- )} ${contextType}`,
62
+ `Set environment variable ${chalk.yellow(
63
+ `${key}${value && !secret ? `=${value}` : ''}`,
64
+ )}${withScope}${withSecret} in the ${chalk.magenta(context || 'all')} ${contextType}`,
64
65
  )
65
66
  }
66
67
 
@@ -88,15 +89,35 @@ const setInMongo = async ({ api, key, siteInfo, value }) => {
88
89
 
89
90
  /**
90
91
  * Updates the env for a site configured with Envelope with a new key/value pair
91
- * @returns {Promise<object>}
92
+ * @returns {Promise<object | boolean>}
92
93
  */
93
- const setInEnvelope = async ({ api, context, key, scope, siteInfo, value }) => {
94
+ const setInEnvelope = async ({ api, context, key, scope, secret, siteInfo, value }) => {
94
95
  const accountId = siteInfo.account_slug
95
96
  const siteId = siteInfo.id
97
+
98
+ // secret values may not be used in the post-processing scope
99
+ if (secret && scope && scope.some((sco) => /post[-_]processing/.test(sco))) {
100
+ error(`Secret values cannot be used within the post-processing scope.`)
101
+ return false
102
+ }
103
+
104
+ // secret values must specify deploy contexts. `all` or `dev` are not allowed
105
+ if (secret && value && (!context || context.includes('dev'))) {
106
+ error(
107
+ `To set a secret environment variable value, please specify a non-development context with the \`--context\` flag.`,
108
+ )
109
+ return false
110
+ }
111
+
96
112
  // fetch envelope env vars
97
113
  const envelopeVariables = await api.getEnvVars({ accountId, siteId })
98
114
  const contexts = context || ['all']
99
- const scopes = scope || AVAILABLE_SCOPES
115
+ let scopes = scope || AVAILABLE_SCOPES
116
+
117
+ if (secret) {
118
+ // post_processing (aka post-processing) scope is not allowed with secrets
119
+ scopes = scopes.filter((sco) => !/post[-_]processing/.test(sco))
120
+ }
100
121
 
101
122
  // if the passed context is unknown, it is actually a branch name
102
123
  let values = contexts.map((ctx) =>
@@ -111,6 +132,10 @@ const setInEnvelope = async ({ api, context, key, scope, siteInfo, value }) => {
111
132
  if (!value) {
112
133
  // eslint-disable-next-line prefer-destructuring
113
134
  values = existing.values
135
+ if (!scope) {
136
+ // eslint-disable-next-line prefer-destructuring
137
+ scopes = existing.scopes
138
+ }
114
139
  }
115
140
  if (context && scope) {
116
141
  error(
@@ -123,12 +148,24 @@ const setInEnvelope = async ({ api, context, key, scope, siteInfo, value }) => {
123
148
  await Promise.all(values.map((val) => api.setEnvVarValue({ ...params, body: val })))
124
149
  } else {
125
150
  // otherwise update whole env var
126
- const body = { key, scopes, values }
151
+ if (secret) {
152
+ scopes = scopes.filter((sco) => !/post[-_]processing/.test(sco))
153
+ if (values.some((val) => val.context === 'all')) {
154
+ log(`This secret's value will be empty in the dev context.`)
155
+ log(`Run \`netlify env:set ${key} <value> --context dev\` to set a new value for the dev context.`)
156
+ values = AVAILABLE_CONTEXTS.filter((ctx) => ctx !== 'all').map((ctx) => ({
157
+ context: ctx,
158
+ // empty out dev value so that secret is indeed secret
159
+ value: ctx === 'dev' ? '' : values.find((val) => val.context === 'all').value,
160
+ }))
161
+ }
162
+ }
163
+ const body = { key, is_secret: secret, scopes, values }
127
164
  await api.updateEnvVar({ ...params, body })
128
165
  }
129
166
  } else {
130
167
  // create whole env var
131
- const body = [{ key, scopes, values }]
168
+ const body = [{ key, is_secret: secret, scopes, values }]
132
169
  await api.createEnvVars({ ...params, body })
133
170
  }
134
171
  } catch (error_) {
@@ -166,13 +203,16 @@ export const createEnvSetCommand = (program) =>
166
203
  'runtime',
167
204
  ]),
168
205
  )
206
+ .option('--secret', 'Indicate whether the environment variable value can be read again.')
169
207
  .description('Set value of environment variable')
170
208
  .addExamples([
171
209
  'netlify env:set VAR_NAME value # set in all contexts and scopes',
172
210
  'netlify env:set VAR_NAME value --context production',
173
211
  'netlify env:set VAR_NAME value --context production deploy-preview',
212
+ 'netlify env:set VAR_NAME value --context production --secret',
174
213
  'netlify env:set VAR_NAME value --scope builds',
175
214
  'netlify env:set VAR_NAME value --scope builds functions',
215
+ 'netlify env:set VAR_NAME --secret # convert existing variable to secret',
176
216
  ])
177
217
  .action(async (key, value, options, command) => {
178
218
  await envSet(key, value, options, command)
@@ -1,5 +1,5 @@
1
1
  import { env } from 'process'
2
2
 
3
- const latestBootstrapURL = 'https://64523ab4e7865600087fc3df--edge.netlify.com/bootstrap/index-combined.ts'
3
+ const latestBootstrapURL = 'https://6494585a67d46e0008867e60--edge.netlify.com/bootstrap/index-combined.ts'
4
4
 
5
5
  export const getBootstrapURL = () => env.NETLIFY_EDGE_BOOTSTRAP || latestBootstrapURL
@@ -135,18 +135,8 @@ export default async function handler({ config, directory, errorExit, func, meta
135
135
  const featureFlags = {}
136
136
 
137
137
  if (metadata.runtimeAPIVersion === 2) {
138
- // For TypeScript we use NFT, otherwise we leave the file untouched with the `none` bundler
139
- const isTypescript = ['.ts', '.mts', '.cts'].includes(path.extname(func.mainFile))
140
-
141
- if (isTypescript) {
142
- functionsConfig['*'].nodeBundler = 'nft'
143
- } else {
144
- // using esbuild is less performant than `none`, but it emits sourcemaps and thus
145
- // enables debugging functions
146
- functionsConfig['*'].nodeBundler = 'esbuild'
147
- featureFlags.zisi_pure_esm = true
148
- featureFlags.zisi_pure_esm_mjs = true
149
- }
138
+ featureFlags.zisi_pure_esm = true
139
+ featureFlags.zisi_pure_esm_mjs = true
150
140
  } else {
151
141
  // We must use esbuild for certain file extensions.
152
142
  const mustTranspile = ['.mjs', '.ts', '.mts', '.cts'].includes(path.extname(func.mainFile))
@@ -75,20 +75,20 @@ const formatLambdaLocalError = (err, acceptsHtml) =>
75
75
  })
76
76
  : `${err.errorType}: ${err.errorMessage}\n ${err.stackTrace?.join('\n ')}`
77
77
 
78
- const processRenderedResponse = async (err, request) => {
79
- const acceptsHtml = request.headers && request.headers.accept && request.headers.accept.includes('text/html')
80
- const errorString = typeof err === 'string' ? err : formatLambdaLocalError(err, acceptsHtml)
81
-
82
- return acceptsHtml
83
- ? await renderErrorTemplate(errorString, './templates/function-error.html', 'function')
84
- : errorString
85
- }
86
-
87
78
  const handleErr = async (err, request, response) => {
88
79
  detectAwsSdkError({ err })
89
80
 
81
+ const acceptsHtml = request.headers && request.headers.accept && request.headers.accept.includes('text/html')
82
+ const errorString = typeof err === 'string' ? err : formatLambdaLocalError(err, acceptsHtml)
83
+
90
84
  response.statusCode = 500
91
- response.end(await processRenderedResponse(err, request))
85
+
86
+ if (acceptsHtml) {
87
+ response.setHeader('Content-Type', 'text/html')
88
+ response.end(await renderErrorTemplate(errorString, './templates/function-error.html', 'function'))
89
+ } else {
90
+ response.end(errorString)
91
+ }
92
92
  }
93
93
 
94
94
  const validateLambdaResponse = (lambdaResponse) => {
@@ -36,9 +36,29 @@ import { generateRequestID } from './request-id.mjs'
36
36
  import { createRewriter, onChanges } from './rules-proxy.mjs'
37
37
  import { signRedirect } from './sign-redirect.mjs'
38
38
 
39
- const decompress = util.promisify(zlib.gunzip)
39
+ const gunzip = util.promisify(zlib.gunzip)
40
+ const brotliDecompress = util.promisify(zlib.brotliDecompress)
41
+ const deflate = util.promisify(zlib.deflate)
40
42
  const shouldGenerateETag = Symbol('Internal: response should generate ETag')
41
43
 
44
+ /**
45
+ * @param {Buffer} body
46
+ * @param {string | undefined} contentEncoding
47
+ * @returns {Promise<Buffer>}
48
+ */
49
+ const decompressResponseBody = async function (body, contentEncoding = '') {
50
+ switch (contentEncoding) {
51
+ case 'gzip':
52
+ return await gunzip(body)
53
+ case 'br':
54
+ return await brotliDecompress(body)
55
+ case 'deflate':
56
+ return await deflate(body)
57
+ default:
58
+ return body
59
+ }
60
+ }
61
+
42
62
  const formatEdgeFunctionError = (errorBuffer, acceptsHtml) => {
43
63
  const {
44
64
  error: { message, name, stack },
@@ -479,7 +499,7 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
479
499
 
480
500
  if (isEdgeFunctionsRequest(req) && isUncaughtError) {
481
501
  const acceptsHtml = req.headers && req.headers.accept && req.headers.accept.includes('text/html')
482
- const decompressedBody = await decompress(responseBody)
502
+ const decompressedBody = await decompressResponseBody(responseBody, req.headers['content-encoding'])
483
503
  const formattedBody = formatEdgeFunctionError(decompressedBody, acceptsHtml)
484
504
  const errorResponse = acceptsHtml
485
505
  ? await renderErrorTemplate(formattedBody, './templates/function-error.html', 'edge function')
@@ -487,6 +507,7 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
487
507
  const contentLength = Buffer.from(errorResponse, 'utf8').byteLength
488
508
 
489
509
  res.setHeader('content-length', contentLength)
510
+ res.statusCode = 500
490
511
  res.write(errorResponse)
491
512
  return res.end()
492
513
  }