Skip to content

ext:export improvements (smarter choice of where to write file to, automatic secret ejection) - #10932

Open
Berlioz wants to merge 6 commits into
mainfrom
vsfan_eject_secrets
Open

ext:export improvements (smarter choice of where to write file to, automatic secret ejection)#10932
Berlioz wants to merge 6 commits into
mainfrom
vsfan_eject_secrets

Conversation

@Berlioz

@Berlioz Berlioz commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@wiz-9635d3485b

wiz-9635d3485b Bot commented Aug 12, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 3 Medium
Software Management Finding Software Management Findings -
Total 3 Medium

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the ext:export command to support exporting Extension instances to Function Kits, introducing new options (--kit and --outputDir) and logic to transfer control of secrets from Extensions to Kits. The review identified a few critical issues: a potential TypeError in ext-export.ts when functions is not defined in firebase.json, another potential TypeError in export.ts when an optional secret parameter is missing from liveParams, and a style guide violation where a user-facing error is logged instead of throwing a FirebaseError.

Comment thread src/commands/ext-export.ts Outdated
Comment on lines +243 to +248
const functionsConfig = firebaseConfig.get("functions");
const functions = Array.isArray(functionsConfig) ? functionsConfig : [functionsConfig];
for (const fn of functions) {
if (typeof fn.kit === "undefined" || typeof fn.instances === "undefined") {
continue;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If functionsConfig is undefined (e.g., when functions is not defined in firebase.json), functions will be initialized as [undefined]. When iterating, fn will be undefined, causing a TypeError: Cannot read properties of undefined (reading 'kit') when evaluating typeof fn.kit. We should handle the case where functionsConfig is falsy or filter out undefined values.

    const functionsConfig = firebaseConfig.get("functions");
    const functions = Array.isArray(functionsConfig)
      ? functionsConfig
      : functionsConfig
      ? [functionsConfig]
      : [];
    for (const fn of functions) {
      if (!fn || typeof fn.kit === "undefined" || typeof fn.instances === "undefined") {
        continue;
      }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the config loader coerces the config to always contain a default functions config if one isn't present

Comment thread src/extensions/export.ts
Comment on lines +170 to +172
const secretName = specParam.param;
const resourceName = liveParams[secretName];
const match = resourceName.match(SECRET_VERSION_NAME_REGEX);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If a secret parameter is optional or not set in liveParams, resourceName will be undefined. Calling resourceName.match(...) will result in a runtime crash (TypeError: Cannot read properties of undefined (reading 'match')). We should add a defensive check to ensure resourceName is defined before attempting to match it.

Suggested change
const secretName = specParam.param;
const resourceName = liveParams[secretName];
const match = resourceName.match(SECRET_VERSION_NAME_REGEX);
const secretName = specParam.param;
const resourceName = liveParams[secretName];
if (!resourceName) {
continue;
}
const match = resourceName.match(SECRET_VERSION_NAME_REGEX);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty sure that can't happen in an ACTIVE extension since there's no way to define a secret param as optional, but it's true that this could be a problem if run on an ERRORED extension for some reason.

Comment on lines +162 to +167
if (instance.state !== "ACTIVE" && !options.force) {
logger.error(
`Extension ${options.instance} is in state ${instance.state}. To export a non-ACTIVE extension, use the --force option.`,
);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the repository style guide, expected user-facing errors should throw a FirebaseError instead of logging an error and returning. This ensures the CLI exits with a non-zero status code and handles the error consistently.

  if (instance.state !== "ACTIVE" && !options.force) {
    throw new FirebaseError(
      `Extension ${options.instance} is in state ${instance.state}. To export a non-ACTIVE extension, use the --force option.`,
    );
  }
References
  1. Throw FirebaseError for expected, user-facing errors. (link)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack

@Berlioz
Berlioz requested a review from ajperel August 12, 2026 23:52
@Berlioz Berlioz changed the title WIP: ext:export improvements ext:export improvements (smarter choice of where to write file to, automatic secret ejection) Aug 12, 2026

@ajperel ajperel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A need to review more closely, but here's some initial feedback.

Comment thread src/commands/ext-export.ts Outdated
Comment thread src/commands/ext-export.ts Outdated
Comment thread src/commands/ext-export.ts Outdated
if (secretCount > 0) {
if (
!(await confirm({
message: `${secretCount} Cloud Secret Manager resources found in export. Transfer control of secrets from Extensions to Kits?`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Kits aren't going to manage secrets. This sounds like something else is going to take it up for auto managing for them.

They must self-manage secrets after this.

Also doesn't help a user understand why this is important. If we're going to prompt we should probably explain why.

?.. in export. Stop Firebase Extensions management of secrets? This is necessary to prevent extension uninstall from deleting potentially migrated secrets."

I don't love this.

I still wonder if it should be a flag rather than a prompt. Maybe worth a quick discussion with @inlined

Comment thread src/commands/ext-export.ts Outdated
Comment thread src/deploy/extensions/secrets.ts

/**
* PATCHes a Secret resource by removing any "firebase-extensions-managed" labels
* and replacing them with "firebase-managed": "functions"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh this is new to me. What's the implication of firebase-managed: functions? Do we do that for other secrets we right from the command line?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we actually have a small collection of bugs because it used to be firebase-managed: true but now it's been split into firebase-managed: functions and firebase-managed: apphosting.

@ajperel ajperel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some mostly nitty comments the substantive thing being we need to make sure we handle secrets not being disassociated from Extensions well.

Also you should add a CHANGELOG and maybe some tests?

)
.option(
`--instance <instanceId>`,
`-e`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As nice as this is, it's a breaking change and I don't want to break anything that currently scripts usage of --instance.

I'd just keep it as is, though you could alias and deprecate if we really wanted.

if (experiments.isEnabled("extMigrationFeatures") && options.mode === "functions") {
if (
experiments.isEnabled("extMigrationFeatures") &&
(options.mode === "functions" || options.mode === "kits")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you took previous feedback and we no longer have "kits"?

if (typeof fn.kit === "undefined" || typeof fn.instances === "undefined") {
continue;
}
for (const [kitId, kitPath] of Object.entries(fn.instances)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: probably better as instanceId, instanceConfigPath or something like that.

Comment thread src/extensions/export.ts
}
const projectId = match.groups.project;
const secretId = match.groups.secret;
await transferSecretToKits(projectId, secretId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhere around this we should have some really good error messaging to users. I really want to avoid the case where users successfully get a .env file but fail to unmanage secrets. In that case if they move forward with the migration and the secrets are deleted they're just totally lost.

We should at the very least warn LOUDLY that secrets will be deleted and they should re-export to try and fix. But maybe we should go so far as to fail writing the .env file so they have to repeat.

newLabels[labelKey] = labelValue;
}
}
} catch (err: any) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Joe's review bot says you should really use unknown instead of any.

@inlined inlined left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it LGTM with the nit that I'm going to need to be able to call some of this code without it going to the file system (because I'll pass it to init instead)

// Write to firebase root if inside a firebase project dir, otherwise <currentDir>/<instanceId>
let writeLocationOpts: UserEnvsOpts;
const writeLocation: UserEnvsOpts = kitExportTarget(instanceId, projectId, options);
if (hasUserEnvs(writeLocation)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me to use this in ext:migrate I'm going to need you to separate the code that generates environment variables from the code that saves them to disk. This is generally a good thing for testability anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding you correctly, this is already implemented in the functionsEnvFromInstance helper defined in src/extensions/export.ts

Comment thread src/commands/ext-export.ts Outdated
writeUserEnvs(convertedEnv, writeLocation);
}

if (secretCount > 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I usually prefer either testing the opposite and returning early or having a helper alone. Helps you avoid pyramid code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack

Comment thread src/commands/ext-export.ts Outdated
const functionsConfig = firebaseConfig.get("functions");
const functions = Array.isArray(functionsConfig) ? functionsConfig : [functionsConfig];
for (const fn of functions) {
if (typeof fn.kit === "undefined" || typeof fn.instances === "undefined") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not valid to use this command to create a new/first instance?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be. The if() on l:247 is more of a type narrowing clause, since the Config made available by the CLI doesn't validate the format by default. It looks like the .src getter on it does validate though, so I'm moving the code to rely on that for readability.

Comment thread src/extensions/export.ts
const resourceName = liveParams[secretName];
if (!resourceName) {
throw new FirebaseError(
`Secret ${secretName} was defined in the extension spec, but is missing in live deployed secrets.`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like something that should never happen. Should it have a different exit code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd forgotten about that. Good catch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants