You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
New application:create command to create an application in one of a few standard forms. Also supports --data for passing a JSON application definition to the API for full custom control.
Adds application:create with standard security profiles and custom JSON configuration, alongside CLI safety, automation, documentation, and test improvements.
Changes:
Adds application creation with profile defaults, CORS configuration, and custom JSON support.
Improves kickstart confirmation, unattended installation, and option naming.
Expands unit/integration coverage and supporting documentation.
File
Description
src/utils.ts
Adds reusable confirmation handling.
src/index.ts
Formatting-only change.
src/commands/kickstart-kill.ts
Adds --yes confirmation flow.
src/commands/kickstart-install.ts
Adds unattended installation options and validation.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
Global CORS changes lack required confirmation, while tenant targeting, origin configuration, validation, and macOS readiness contain functional defects.
Apply authorized origins to system CORS configuration
src/commands/application-create.ts:238
--authorized-origin-url is advertised as CORS configuration, but this only writes the application's OAuth authorizedOriginURLs. ensureCorsHeaders() updates global allowedHeaders and enabled, never corsConfiguration.allowedOrigins, so browser requests from a newly supplied origin remain blocked unless it was already configured separately. Merge these origins into system CORS (with deduplication) or change the option's contract.
Honor tenant ID supplied in application data
src/commands/application-create.ts:254
Honor a tenant supplied inside --data. The help says --tenant-id overrides --data, but the client receives only the flag value; without the flag, no tenant header is sent and multi-tenant FusionAuth instances may reject the request or target the API key's default tenant even when application.tenantId is present.
JSON.parse may return null, an array, or a scalar, but the cast accepts all of them as Application. For example, --data null then crashes at application.id, while arrays/scalars can be sent as malformed API payloads. Validate that the parsed value is a non-null, non-array object and return a clear input error before applying overrides.
- import-generate: detect deprecated flags in --flag=value form, not just bare --flag
- kickstart-install: replace setTimeout-chained install steps with sequential
awaited steps so errors propagate through try/catch and ordering is
deterministic; also await createKickstart (was previously fire-and-forget)
- utils: confirmOrExit now requires both stdin and stdout to be TTYs before
treating the session as interactive, and normalizes confirmation input
(trims whitespace, accepts y/yes case-insensitively)
- utils.ts: extract isConfirmationAccepted() as a pure, exported function so
the accept/reject decision logic can be unit tested directly without
simulating a real TTY
- kickstart-kill.ts: export action() and add an injectable deps parameter
(isDockerInstalled, confirmOrExit, spawn) so tests can exercise the
confirmation gating without touching real docker or exiting the process
- add __tests__/utils.test.js covering isConfirmationAccepted and the
yes-bypass / non-interactive TTY-detection paths of confirmOrExit
- add __tests__/commands/kickstart-kill.test.js covering docker-not-installed,
CLI_DIR mismatch, --yes bypass, and confirm-rejected gating paths
- wire both new test files into the test and test:unit npm scripts
- extract getDeprecatedFlagUsage(argv) as a pure, exported function so the
deprecation-detection logic is testable without mocking process.argv or
console.warn
- export DEPRECATED_FLAGS for use in tests
- add __tests__/commands/import-generate.test.js covering: no deprecated
flags used, bare --flag and --flag=value forms detected, multiple
deprecated flags detected together, new kebab-case form not flagged,
and that both the deprecated and current flag spellings populate the
same underlying Commander option property
- wire the new test file into the test and test:unit npm scripts
- Fix inverted localhost/container-IP fallback order in integration
test setup's auth-readiness check
- Gate CORS system-configuration mutation behind --yes/confirmOrExit
per the Risky Operations Policy
- Make --name optional; required only for --profile, preserved from
--data JSON unless explicitly overridden
- Document that applicationId/clientId are intentionally identical
(FusionAuth never accepts clientId as input)
- Remove NODE_ENV-conditional exit from executeApplicationCreate so
it always returns a result per its documented contract; thread the
raw error through to the CLI wrapper for field-level error detail
Re: "Test the dist resources path in addition to the source fallback" (kickstart-install.test.js:354, previously-missed finding, no dedicated inline thread).
Confirmed — this test always imports src/commands/kickstart-install.js via tsx, so __dirname was fixed to .../src/commands for the whole run. I verified directly that calling the function from that import only ever exercised the src-fallback branch; the dist-layout branch (and the error-throw path) had zero coverage despite the comment's claim.
Fixed in 1bd1254: added an injectable baseDir parameter to resolveResourcesDir() (defaulting to the real __dirname, so production behavior is unchanged), and replaced the old test with 4 synthetic temp-directory tests covering all three logic branches (dist found / src fallback found / neither found, including the previously-untested throw path). Also added a separate test that imports the real compiled dist/commands/kickstart-install.js directly and verifies it resolves correctly against the actual build output — this one skips gracefully (not fails) if dist/ hasn't been built yet, so test:unit still works without requiring a build first, while still being meaningful in CI (which always builds before testing).
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
The bridge-IP fallback depends on a generated Docker container name that is not stable across Compose project configurations.
Review effort: Balanced Findings: None
Previously missed (1)
In code that hasn't changed since last review
Resolve FusionAuth container ID via Compose instead of generated name
__tests__/integration/setup.js:24
This fallback relies on Docker Compose's generated container name, but the compose file does not declare container_name; the name changes when the project name is overridden (for example with COMPOSE_PROJECT_NAME). In that case both docker inspect calls silently fail and URL resolution falls back to localhost, defeating the new bridge-IP fallback. Resolve the fusionauth service container ID via docker compose ps -q fusionauth and inspect that ID in both lookup paths instead of hard-coding the generated name.
Two previously-missed findings from the same review, neither acted on
across two review cycles -- not a deliberate decision, just missed.
CONTAINER_NAME hard-coded Docker Compose's default generated container
name ('{project}-{service}-{index}'). If COMPOSE_PROJECT_NAME is set,
the real container name differs, both docker inspect calls silently
fail (caught by empty catch blocks), and the bridge-IP fallback this
PR added is defeated without any visible error. Replaced with
resolveContainerId(), which resolves the real ID via
`docker compose ps -q fusionauth`, independent of naming conventions.
Verified end-to-end via a full local docker-based integration run --
the bridge-IP fallback message still appears correctly, confirming
the dynamic resolution works.
COMPOSE_DIR used new URL(...).pathname, which leaves special characters
like spaces percent-encoded (e.g. '%20') rather than decoding them --
not a valid filesystem path component. Verified empirically that
fileURLToPath() correctly decodes it instead. Also replaced the
`cd ${COMPOSE_DIR} && ...` string-concatenation pattern (5 call sites)
with execAsync(cmd, { cwd: COMPOSE_DIR }), avoiding shell-quoting
issues with the path entirely rather than just moving them around.
Re: "Resolve FusionAuth container ID via Compose instead of generated name" (setup.js:24, previously-missed finding, now recurring across two review cycles).
For transparency: this one was not a deliberate decision to skip — it was simply missed. It only ever surfaced as a summary-only "previously missed" bullet with no dedicated inline thread to reply to directly, and it slipped through across both rounds.
Confirmed the issue is real: CONTAINER_NAME hard-coded Compose's default generated name; if COMPOSE_PROJECT_NAME is set, the real name differs and both docker inspect calls silently fail, defeating the bridge-IP fallback with no visible error.
Fixed in da3659d: added resolveContainerId(), which resolves the real container ID via docker compose ps -q fusionauth instead of guessing the name. Verified end-to-end via a full local docker-based integration run — the bridge-IP fallback still engages correctly.
While investigating, I also found a second, closely related finding from the same review that was equally unaddressed ("Use fileURLToPath and cwd to support checkout paths containing spaces", setup.js:25) and fixed that in the same commit: COMPOSE_DIR now uses fileURLToPath() instead of .pathname (verified empirically that .pathname left spaces percent-encoded rather than decoding them), and all 5 cd ${COMPOSE_DIR} && ... call sites now use execAsync(cmd, { cwd: COMPOSE_DIR }) instead, avoiding shell-quoting issues with the path entirely.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
Duplicate supplied origins can pollute global CORS configuration, and new resource tests leak temporary directories.
Review effort: Balanced Findings: None
Previously missed (2)
In code that hasn't changed since last review
Deduplicate authorized origins before updating CORS configuration
src/commands/application-create.ts:195
When --authorized-origin-url contains the same origin more than once, each copy passes this filter because it is compared only with the pre-existing allowlist. The patch then writes duplicate entries into the system-wide CORS configuration. Deduplicate the supplied origins before computing the additions.
Each test in this block creates a directory under the system temp directory, but none of those directories are removed. Repeated local and CI runs therefore leave four directory trees behind per run; track them and clean them after each test.
Duplicate --authorized-origin-url values were only ever compared
against the pre-existing system CORS allowlist, not against each
other, so passing the same origin twice wrote a duplicate entry into
the system-wide CORS configuration. Deduped the supplied origins via
[...new Set(authorizedOrigins)] before filtering. Verified empirically
before/after, and added a regression test asserting the origin appears
exactly once in the PATCH payload.
Also fixed resolveResourcesDir()'s unit tests leaking temp directories
on every run -- mkTempDir() created a real directory under the OS temp
dir in each of the 4 tests but never removed any of them. Confirmed
this was a real, accumulating leak: found 16 leftover directories from
prior test runs still on disk. Now tracks created dirs and removes
them in afterEach; verified a fresh run leaves zero behind.
Re: "Deduplicate authorized origins before updating CORS configuration" (application-create.ts:195, previously-missed finding, no dedicated inline thread).
Confirmed — authorizedOrigins was only ever compared against the pre-existing allowlist, not against itself, so passing the same --authorized-origin-url twice wrote a duplicate entry into the system-wide CORS configuration. Verified empirically before/after.
Fixed in d19dfc9: deduped via [...new Set(authorizedOrigins)] before filtering. Added a regression test asserting the origin appears exactly once in the PATCH payload.
Re: "Clean up temporary directories created by tests" (kickstart-install.test.js:363, previously-missed finding, no dedicated inline thread).
Confirmed — none of the 4 resolveResourcesDir() tests cleaned up their mkTempDir()-created directories. Checked disk and found 16 leftover directories from prior test runs still present, confirming this was a real, accumulating leak.
Fixed in d19dfc9: mkTempDir() now tracks each created directory, and a new afterEach removes them. Cleaned up the 16 pre-existing leftovers and verified a fresh run leaves zero behind.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
Repeated termination signals are ignored and can leave a hung teardown process impossible to stop normally.
Review effort: Balanced Findings: None
Previously missed (1)
In code that hasn't changed since last review
Exit immediately on repeated termination signals
__tests__/integration/setup.js:67
Because installing a signal handler disables Node's default termination behavior, returning on a second signal can make the test runner impossible to stop if the first docker compose down -v hangs. A repeated Ctrl+C/SIGTERM should exit immediately rather than being ignored.
Once a SIGINT/SIGTERM listener is registered, Node no longer applies
its default "a second Ctrl+C just kills the process" behavior on its
own -- the listener has full responsibility. The early `return` on a
repeated signal while handlingTerminationSignal was already true meant
that if forceTeardown()'s `docker compose down -v` call hung (it has
no timeout, unlike every other network call in this file), every
subsequent Ctrl+C/SIGTERM was silently swallowed, leaving no way to
interrupt the process short of `kill -9` from another terminal.
Verified both the bug and the fix with a standalone repro harness
simulating a permanently-hung teardown: the old logic left the process
running indefinitely after a second SIGINT; the new logic force-exits
with the expected code (130/143) immediately. Also ran the full
integration suite to confirm no regression in normal (non-hung)
teardown.
Re: "Exit immediately on repeated termination signals" (setup.js:67, previously-missed finding, no dedicated inline thread).
Agreed, this is a real risk — confirmed it with a standalone repro harness simulating a permanently-hung forceTeardown() (which has no timeout on its docker compose down -v call, unlike every other network call in this file): with the old code, a second SIGINT after the first was silently swallowed and the process stayed stuck forever, with no way to interrupt it short of kill -9 from another terminal. Verified the fix resolves this — second signal now force-exits immediately with the expected code.
Note: deliberately not adding automated test coverage for this. Testing real process-level SIGINT/SIGTERM delivery and exit-code behavior from within the test suite itself is impractical with this project's existing test setup (these functions are private/unexported, and nothing else in this codebase attempts to test actual signal delivery) — verification here was done manually via an external repro harness instead, which is captured in this comment for the record.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
The readiness loop performs potentially blocking Docker fallback discovery before testing the healthy localhost endpoint.
Review effort: Balanced Findings: None
Previously missed (1)
In code that hasn't changed since last review
Test localhost before slow Docker bridge resolution
__tests__/integration/setup.js:290
The bridge fallback is still resolved before localhost is tested: every iteration runs docker compose ps and docker inspect, then tries the URLs. A slow or hung Docker CLI can therefore block readiness even when the authenticated localhost endpoint is already healthy. Attempt localhost first and only resolve/inspect the container after that request fails or returns non-OK.
Re: "Test localhost before slow Docker bridge resolution" (setup.js:290, previously-missed finding, no dedicated inline thread).
Reviewed this on the merits and are declining it.
This is an ordering nitpick on two cheap, local docker CLI calls (ps/inspect, normally tens of milliseconds) ahead of an HTTP fetch inside a loop that already has a 5-second per-attempt timeout and a 30-second total budget. Localhost is still tried and still wins the moment it's healthy — nothing here is silently incorrect, unlike the earlier (legitimate, fixed) bug where the bridge IP unconditionally overrode checkUrl and localhost was never actually reached at all. The theoretical "slow/hung Docker CLI blocks readiness" scenario this flags is true in the abstract but immaterial in practice given the existing timeout budget, and this exact file has already been restructured several times this session for real correctness/security findings.
At this point, flagging ordering of sub-50ms local subprocess calls inside an already-bounded retry loop as a "needs a closer look" item is past the point of being useful signal. Perfection is the enemy of done — this PR has been through extensive, substantive review across dozens of real findings (security validation gaps, CORS protocol correctness, resource leaks, signal handling), and this one does not clear the bar of being worth the risk of yet another change to already-well-tested timing-sensitive retry logic. Not acting on this.
The reason will be displayed to describe this comment to others. Learn more.
All in all, not setting off any alarm bells, LGTM with a couple output tweak suggestions
Need to add betaWarning() to the outputs, and add that they're in beta to the readmes and helps if we want this to be beta instead of just regular new functionality (i.e. making sure folks know this could go away or change)
Root cause: the FusionAuth SDK rejects with a ClientResponse instance,
which does NOT extend Error. All three wrapError() call sites in
application-create.ts built their message via
"e instanceof Error ? e.message : String(e)" -- since e is never
instanceof Error for a real SDK rejection, this always fell to
String(e), which for a plain class instance produces the literal
"[object Object]". Reproduced the exact reported repro (creating an
application with a duplicate name) character for character before
fixing, and confirmed the new test fails against the old code.
Added describeError() to utils.ts, reusing the existing
isClientResponse()/isErrors() guards to correctly unpack
fieldErrors/generalErrors, a nested network-level Error, or fall back
to "HTTP <statusCode>" -- instead of duplicating that logic locally.
Hardened isClientResponse()/isErrors() to not throw on null/undefined
input, now that they're used more broadly via describeError(). Used
describeError() at all three wrapError() sites.
Also replaced reportError()'s final fallback (JSON.stringify via
toJson) with util.inspect, which handles circular references and
non-JSON-serializable values gracefully instead of throwing or
silently dropping them, for genuinely unknown error shapes.
Added betaWarning() to application:create's action() (matching the
existing convention used by all kickstart:* commands), updated the
README's new Applications heading to "(beta)", and added blank-line
spacing before the success message and before the Next Steps box per
review feedback.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Error reporting still throws when given truthy primitive error values.
0 open findings
Previously missed (1)
In code that hasn't changed since last review
Guard in check against primitive errors before unknown-shape fallback
src/utils.ts:131
The new “unknown shape” fallback is still unreachable for truthy primitive errors: reportError('...', 42) reaches 'message' in error first, which throws a TypeError for numbers, booleans, bigints, and symbols. Guard the in check to objects/functions so these values reach inspect() instead of crashing error reporting.
f54f856 fixed "[object Object]" by baking describeError()'s full
detail into wrapError()'s message, which became ApplicationCreateResult
.error. But reportError() already does its own unwrapping of rawError
to print structured fieldErrors/generalErrors detail -- so once .error
also carried that same detail, the CLI printed it twice (once as the
flattened message, once via reportError's own per-field breakdown).
The redundancy was a symptom of putting a presentation-layer concern
(a fully flattened, human-readable string) into a data field. Reverted
wrapError()'s message construction to short, static, per-stage labels
("Error creating application", etc.) with no appended detail -- this
also means the original [object Object] bug can no longer occur by
construction, since the message no longer attempts to coerce the
rejection into a string at all. The real detail now flows through
rawError alone, and reportError()'s existing (unchanged) formatting
handles it correctly for both single- and multi-error cases, with no
new branching logic needed anywhere.
describeError() is now unused in production code (confirmed via
repo-wide search) -- removed it from utils.ts along with its tests.
Updated the one test that asserted on .error's flattened detail to
instead assert structurally on rawError.exception, which is more
robust and doesn't couple the test to any particular string-formatting
choice.
Verified against the real compiled CLI with both a single-field-error
and a multi-field-error mocked response: single case now prints
exactly one line of detail (previously two identical lines); multi
case correctly prints each field error on its own line, matching the
original pre-regression behavior.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Unknown primitive errors can still crash reportError, and the integration timeout documentation is stale.
0 open findings
Previously missed (2)
In code that hasn't changed since last review
Guard message property check for primitive thrown values
src/utils.ts:94
This fallback still throws before reaching inspect when the thrown value is a number, bigint, or symbol: the preceding 'message' in error check requires an object RHS. Guard that check by type so the newly documented unknown-value handling actually works for these values.
Update integration test timeout documentation to 240000ms
__tests__/integration/setup.js:22
The timeout was doubled here, but __tests__/integration/README.md:115-117 still tells contributors that the default is 120000ms. Update that troubleshooting guidance to 240000ms so it matches the runner.
The reason will be displayed to describe this comment to others. Learn more.
LGTM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
New application:create command to create an application in one of a few standard forms. Also supports --data for passing a JSON application definition to the API for full custom control.