Skip to content

fix(cli): separate transport auth from operation payload for add_user/alter_user - #1873

Merged
kriszyp merged 3 commits into
mainfrom
fix/cli-non-interactive-auth
Jul 28, 2026
Merged

kriszyp merged 3 commits into
mainfrom
fix/cli-non-interactive-auth

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 20, 2026 •

Copy link
Copy Markdown
Member

What / why

add_user/alter_user reused the same username=/password= CLI args as both the HTTP Basic auth credentials and the operation payload, so the CLI tried to log in as the user being created. That made non-interactive (CI/CD) add_user/alter_user — authenticating as an existing admin while creating/altering a different user — impossible. Env-var auth (HARPER_CLI_USERNAME/HARPER_CLI_PASSWORD) was also silently overridden by the payload params.

Root cause and fix, entirely in bin/cliOperations.ts:

  • Added dedicated auth args auth_username=/auth_password= that always win the auth leg, and added them to TRANSPORT_ONLY_FIELDS so they never serialize into the request body.
  • Removed username/password from TRANSPORT_ONLY_FIELDS — they're legitimate operation-payload fields for several ops (add_user, alter_user, create_authentication_tokens), not just transport. They remain the auth fallback for ops where they legitimately are the auth (e.g. plain create_table ... username= password=), preserving backward compatibility.
  • Flipped auth precedence so env-var auth (HARPER_CLI_USERNAME/HARPER_CLI_PASSWORD, CLI_TARGET_USERNAME/CLI_TARGET_PASSWORD) beats plain username=/password= when no dedicated auth_* arg is set — otherwise a configured CI login is silently shadowed by an operation's own payload fields. Confirmed with @kriszyp before implementing (this is a deliberate behavior change, called out in the issue).
  • Fixed the non-deploy JSON body path (body = req), which sent the entire raw, unstripped req object as the wire body — now uses the same operationFields() stripping the deploy paths already used, so auth_username/auth_password (and target/rejectUnauthorized/etc.) never leak into any request body.
  • Updated the CLI HELP text (bin/harper.ts) to document the new args and env-var auth for the non-interactive path. Companion docs: HarperFast/documentation#613.
 const TRANSPORT_ONLY_FIELDS = new Set([
 	'target',
-	'username',
-	'password',
+	'auth_username',
+	'auth_password',
 	'rejectUnauthorized',
 	...
 ]);
...
 			username:
-				req.username || target.username || process.env.HARPER_CLI_USERNAME || process.env.CLI_TARGET_USERNAME,
+				req.auth_username ||
+				target.username ||
+				process.env.HARPER_CLI_USERNAME ||
+				process.env.CLI_TARGET_USERNAME ||
+				req.username,
...
 		} else {
-			body = req;
+			body = operationFields(req);
 		}

Test plan

  • Added dedicated auth args vs operation payload fields (harper#1872) describe block in unitTests/bin/cliOperations.test.js: auth_username/auth_password win the Basic-auth header while add_user payload username/password stay in the body; backward-compat fallback to plain username/password when nothing else is configured; env-var auth beats payload username/password; auth args stripped from both the non-deploy JSON body and the multipart directory-deploy body.
  • Full unitTests/bin/cliOperations.test.js suite passes (30/30).
  • Ran the broader unit suite (test:unit:main pattern): 3743 passing, 10 pre-existing failures in unitTests/components/globalIsolation.test.js unrelated to this change (component sandbox path resolution, reproduces identically on main).
  • oxlint and prettier --check clean on all three changed files.

Notes for reviewers

Follow-up: review round 2 (commit 9473f61ba)

Three issues from review, plus three found by an outside-model pass on the fix itself:

1. Saved harper login token was still unreachable for add_user/alter_user (blocker). The payload fallback was folded into target.username before the Authorization mode was chosen, and if (target?.username) selects Basic ahead of the saved-token branch — so harper login followed by add_user username=newuser password=... authenticated as the user being created and 401'd. Authentication now resolves in this order:

  1. explicitly configured credentials (dedicated auth_* args → target-URL userinfo → HARPER_CLI_* → CLI_TARGET_*)
  2. the saved harper login operation token
  3. the legacy username=/password= payload fallback — now requiring both, so a lone username= (drop_user username=bob) stays payload rather than becoming half a credential

2. Credentials are resolved as an atomic pair, never composed across sources. resolveTransportCredentials() walks the sources and returns the first that supplies anything, so auth_username=admin can no longer be paired with an add_user payload password. A source counts as configured once either half is supplied, empty or not, so a blank CI variable doesn't read as "absent". An incomplete explicit source (args, URL userinfo) is a hard error; an incomplete env pair warns on stderr and is skipped, because a lone HARPER_CLI_USERNAME/HARPER_CLI_PASSWORD is a supported harper login idiom and shouldn't break every later operation in the same shell or CI job.

3. Passwords no longer reach logs or terminal output. The parsed CLI request is redacted before logger.trace, and a password embedded in target=https://admin:secret@host is masked in the Connecting to ... line and anywhere the target is logged. Help text now leads with the env-var form and states that a password passed as an argument is exposed to shell history, process listings and CI logs.

Also fixed in bin/login.ts (found while verifying the above):

  • harper login mixed env namespaces — username from CLI_TARGET_USERNAME, password from HARPER_CLI_PASSWORD — which is the same cross-pairing hazard. Whichever namespace supplies anything now owns both halves; a half it doesn't set is prompted for. This also flips login's namespace preference to HARPER_CLI_*-first, matching the rest of the CLI (only observable if both complete pairs are set to different values).
  • Login now passes its credentials as dedicated transport auth as well as payload fields. Without that, moving the saved token ahead of the payload fallback meant a re-login after expiry would try to refresh the very token it was replacing and could exit with "Refresh token expired... please run harper login again".

Follow-up test plan

  • unitTests/bin/cliOperations.test.js: 42 passing. New coverage — saved token wins for both add_user and alter_user while payload creds stay in the body; lone payload username= is not auth; CLI_TARGET_* pair still authenticates; lone auth_username, lone auth_password, empty auth_password and both-empty auth args all fail with a clear error; incomplete env pair warns and falls through to the saved token; URL password masked in the connection log; redactCredentials() unit tests.
  • unitTests/bin/login.test.js: new credential sources block — no cross-namespace pairing, CLI_TARGET_*-only still works, credentials sent as dedicated transport auth.
  • npm run test:unit:main: 3758 passing, 10 pre-existing unitTests/components/globalIsolation.test.js failures (verified identical with these changes stashed).
  • oxlint and prettier --check clean.

Open question for reviewers

The asymmetry in 2 is a judgment call: incomplete explicit credentials are fatal, incomplete env credentials only warn. The alternative (fatal everywhere) would break export HARPER_CLI_USERNAME=admin; harper login; harper <op> — every operation after the login would fail even though the saved token is perfectly good. Happy to make it uniformly fatal if reviewers prefer that.

Note on the extra commit 676990fff

676990fff is not part of this change. It's a one-line type widening in resources/DatabaseTransaction.ts (recordCommitLatency's parameter to Promise<unknown>) that fixes the pre-existing npm run build failure from #1688, which was turning the downstream Next.js adapter integration check red on this PR for reasons unrelated to it. The same fix is queued as its own PR against main; once that lands, this commit is redundant and can be dropped in the rebase.

Refs #1872

🤖 Generated with Claude Code

@kriszyp
kriszyp requested review from DavidCockerill and heskew July 20, 2026 16:10

@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 introduces dedicated authentication arguments (auth_username and auth_password) to the CLI operations, preventing payload fields (such as those in add_user or alter_user) from being mistakenly used as transport credentials. It also updates the CLI help documentation and adds unit tests to verify the priority of these credentials and that they are stripped from the request bodies. Feedback was provided regarding a potential credential cross-contamination risk where a partially specified higher-priority credential source could cause the fallback logic to incorrectly mix admin usernames with payload passwords, along with a suggested fix to guard the fallback.

Comment thread bin/cliOperations.ts Outdated
Comment thread bin/cliOperations.ts Outdated
@claude

claude Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found. The only change since the last review pass (commit 6c6bd5b) is documentation-only — HELP text noting the stale-token 401 behavior and a comment clarifying SECRET_FIELDS is a by-name allowlist. Both prior inline findings (credential cross-contamination) remain resolved via resolveTransportCredentials().

@kriszyp
kriszyp marked this pull request as ready for review July 21, 2026 19:30
kriszyp and others added 2 commits July 22, 2026 13:37
…/alter_user

add_user/alter_user reused the same username=/password= CLI args as both
the HTTP Basic auth credentials and the operation payload, so authenticating
as an admin while creating/altering a different user was impossible
non-interactively — the payload values won the auth race, and env-var auth
(HARPER_CLI_USERNAME/PASSWORD) was silently overridden.

Give transport auth its own namespace (auth_username=/auth_password=),
always preferred for the auth leg, and add them to TRANSPORT_ONLY_FIELDS so
they never serialize into the request body. Env-var auth now also beats
plain username=/password= when no dedicated auth arg is set, so a
configured CI login isn't shadowed by an operation's own payload fields;
username=/password= remain the auth fallback for ops where they legitimately
ARE the auth. Also fixes the non-deploy body path, which sent the raw,
unstripped req object as the wire body.

Refs #1872

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…fields

Follow-up to the review of the non-interactive auth fix. Three changes:

- The saved `harper login` token now outranks the legacy `username=`/`password=`
  payload fallback. Previously any payload `username=` was folded into the target
  before the Authorization mode was picked, so `harper login` followed by
  `add_user username=newuser password=...` authenticated as the user being
  created and 401'd, leaving the interactive path with the original collision.
- Credentials are resolved as an all-or-nothing pair from one source (dedicated
  args, URL userinfo, or an env-var namespace) instead of per-field `||` chains,
  which could pair one source's username with another's secret. An incomplete
  explicit pair is a hard error; an incomplete env pair warns and is skipped,
  since a lone HARPER_CLI_USERNAME/PASSWORD is a supported `harper login` idiom.
  `harper login` likewise no longer mixes the HARPER_CLI_* and CLI_TARGET_*
  namespaces, and prefers HARPER_CLI_* like the rest of the CLI.
- Passwords no longer reach the logs: the parsed CLI request is redacted before
  the trace log, and a password embedded in `target=` is masked wherever the
  target is printed. Help text leads with the env-var form and notes that a
  password passed as an argument is exposed to shell history and CI logs.

`harper login` now also sends its credentials as dedicated transport auth, so a
re-login after expiry authenticates as the caller instead of trying to refresh
the very token it is replacing.

Refs #1872

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
@kriszyp
kriszyp force-pushed the fix/cli-non-interactive-auth branch from 676990f to 7cbcd5a Compare July 22, 2026 19:38

@DavidCockerill DavidCockerill 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.

Read through this carefully — the design is sound and the test coverage is genuinely strong (42 assertions across two files). No correctness bugs found. I traced the auth control flow, the atomic credential resolution, and the redaction paths, and confirmed utility/common_utils.ts httpRequest does no logging, so the Basic header can't leak downstream. The only programmatic caller (bin/login.ts) is updated, and the dormant .mjs test is describe.skip. Not requesting changes.

One thing I want to sign off on explicitly rather than let through silently, plus a few notes.

The auth-precedence change is a real behavior change on a stable CLI, and it deserves a conscious yes. Env-var auth and the saved harper login token now outrank payload username=/password=, where the payload previously won (bin/cliOperations.ts:393).

In plain terms: if someone's CI was quietly leaning on username=admin password=… to authenticate — say for create_table — and that same shell also had HARPER_CLI_USERNAME exported for something else, the command now authenticates as the env var instead of the argument. Almost always that's the same admin and nothing visibly changes. But it's exactly the kind of silent "who am I authenticating as" shift that should be a deliberate decision on an auth path rather than a surprise.

It's intended, it's yours, and it's tested — so consider this me blessing it rather than flagging it. Recording it here so the decision is on the record if someone hits it later.

The docs follow-up matters more than usual because of that. New auth_username=/auth_password= args, env-var auth, and new precedence rules are all user-facing. Is the documentation-repo follow-up tracked yet? I'd rather it not lag far behind the merge.

Smaller notes, none blocking:

  1. Incomplete-env warning is noisy (cliOperations.ts:171). Exporting a lone HARPER_CLI_USERNAME is a documented harper login idiom (password prompted), but in a non-login op it emits Ignoring incomplete credentials: … on stderr for that command and every later one in the shell. Consider suppressing it when a saved token or other complete source is going to satisfy the request anyway.
  2. redactCredentials is field-name-based, not exhaustive (cliOperations.ts:225). SECRET_FIELDS is {auth_password, password} plus target-URL userinfo — right for this PR's scope, but any other secret-bearing arg (a token, a key) would still reach logger.trace. Worth a comment saying so, so nobody assumes the trace log is safe by construction.
  3. Stale-token edge worth a line in the help/docs (cliOperations.ts:438). If a saved token exists and refresh fails with a non-401, Authorization stays set to the expired Bearer, so the legacy payload-credential fallback is skipped and the request 401s — where payload creds used to work. That's a coherent consequence of making the token outrank the payload (i.e. the point of the fix), and harper logout / auth_* is the escape hatch — it just deserves to be written down.
  4. On the asymmetry you flagged (incomplete explicit creds fatal, incomplete env creds warn-and-skip): agreed with your default. Failing every op after a bare export HARPER_CLI_USERNAME would be worse. Blessing it rather than letting it pass unremarked.
  5. Housekeeping: you noted 676990fff (the DatabaseTransaction.ts type widening) isn't part of this change and gets dropped on rebase — worth confirming it's gone at merge so it doesn't ride in on an auth PR.

Note on review coverage: this one got a Claude-only pass — both outside-model legs were unavailable at the time. Codex is working again now, so if you'd like a second opinion on the auth surface before merging, say the word and I'll run it.

— Reviewed by DAIvid (Claude Opus 5)

Follow-up to review of the non-interactive auth fix (PR #1873):
- HELP text now notes that a saved harper login token outranks
  username=/password=, so a stale token that fails to refresh 401s
  rather than falling back to the payload creds — harper logout or
  auth_username=/auth_password= is the escape hatch.
- Noted that SECRET_FIELDS is a by-name allowlist, not exhaustive, so
  any future secret-bearing arg needs to be added explicitly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kriszyp

kriszyp commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Addressed two of the small, non-blocking notes from @DavidCockerill's review in 6c6bd5b:

  • HELP text (bin/harper.ts) now documents the stale-token 401 edge case (a saved harper login token outranks username=/password=, so a refresh failure 401s instead of falling back to payload creds — harper logout or auth_username=/auth_password= is the escape hatch).
  • Comment on SECRET_FIELDS (bin/cliOperations.ts) now notes it's a by-name allowlist, not exhaustive, so future secret-bearing args need to be added explicitly.

Left the "noisy incomplete-env warning" suggestion (suppress it when a saved token/fallback ends up authenticating anyway) as-is — deferring/suppressing that warning would contradict the existing warns and falls through to the saved token when only one env var of a pair is set test, which locks in today's "always warn" behavior as intentional. That's a design call, not a mechanical fix, so leaving it for @kriszyp to weigh in on if he wants it changed.

The two inline review threads (Gemini + earlier Claude pass on atomic credential resolution) were already resolved in 9473f61ba.

— Claude Sonnet 5

@DavidCockerill DavidCockerill 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.

Approving at 6c6bd5b. The delta since my last pass is six lines of comment and help text across two files — zero logic change, CI green on Node 22/24/26.

Everything I raised is closed:

  • The auth-precedence change is documented, and documentation#613 covers all three user-facing pieces — the auth_* args, env-var auth, and the precedence order — plus stale-token recovery. That was my main ask, since the precedence shift silently changes who a command authenticates as.
  • redactCredentials' field-name-based scope is now noted in a comment.
  • The stale-token fallback behaviour is written down.
  • The stray 676990fff DatabaseTransaction.ts commit is gone; the merge-base is now its standalone fix on main.

Re-verified the security properties at this head: no credential reaches a log, redaction still applied, Basic header still not logged. I also chased and refuted one candidate of my own — a URL parse error potentially leaking target userinfo.

Only open item is the incomplete-env warning noise, which you deliberately deferred; the current test locks in always-warn, so it wants a design call rather than a quick change. Happy to file it as a follow-up.

— Reviewed by DAIvid (Claude Opus 5)

@kriszyp
kriszyp merged commit 3abb26d into main Jul 28, 2026
45 of 46 checks passed
@kriszyp
kriszyp deleted the fix/cli-non-interactive-auth branch July 28, 2026 23:10
kriszyp added a commit that referenced this pull request Sep 11, 2026
…2545)

* test: promote three QA regression anchors from the qa-explorer bank

Adds three integration suites, no product code.

- integrationTests/resources/qa716-lingering-write-commit.test.ts (QA-716 / P-493) pins
  harper#1860: writes staged across three tables in one request transaction, behind a paged
  secondary-index iterator the handler never drains, must be durable and index-consistent the
  moment the request is acked. Additive to unitTests/resources/lingeringWriteCommit.test.js,
  which covers the single-table case against the transaction object in isolation.
- integrationTests/security/qa627-cli-user-auth-separation.test.ts (QA-627 / P-409) pins
  harper#1872, fixed by #1873: CLI transport auth resolves separately from the
  add_user/alter_user payload, as an atomic pair per source. The banked candidate asserted the
  pre-fix defect; inverted here to assert the shipped behaviour.
- integrationTests/mqtt/qa681-mqtt-shared-subscriptions.test.ts (QA-681 / P-460) pins that
  $share subscriptions are explicitly REFUSED with a SUBACK reason code rather than accepted as
  an inert filter, that ordinary fan-out stays complete and duplicate-free alongside them, and
  that a durable session loses nothing across an abrupt mid-stream drop.

Refs #1860
Refs #1872

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZPy6UYZtwxhzLgimHWbGD

* test: address pre-push review on the QA promotion anchors

- QA-681 Q4 polls the durable-session resume on sequence COVERAGE rather than a
  delivery count. QoS-1 is at-least-once, so a resume may replay a message that was
  unacked when the transport died; counting deliveries reaches the expected total
  while a later sequence is still in flight, and the run would then fail a session
  that recovers everything.
- QA-627's CLI home moves under the harness dataRootDir, so instance teardown removes
  it rather than leaking a tmpdir entry when the before hook throws, and a spawn
  failure's string `code` (ENOENT and friends) no longer reaches a numeric contract.
- Drop a stale fixture comment and several that restated adjacent code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZPy6UYZtwxhzLgimHWbGD

* test: remove cross-arm state races from the QA promotion anchors

- QA-681 Q3 publishes to its own topic. Sharing Events/stream with Q1 left Q1's last
  row as that topic's current record, and Harper's subscribe path yields it from a
  detached iterator that can lose the race to SUBACK — landing a Q1 message in Q3's
  count on a loaded shard.
- QA-681 Q4 converges on the dropped client's socket actually being down before
  publishing past the drop point, so the boundary assertion is deterministic rather
  than a race with the next publish.
- QA-716 Q2 drops its index assertion: search_by_value goes through the same
  materialization as the base dump, hiding an expired row and dropping one whose base
  record is gone, so once the dump is empty the index query is empty too whatever the
  index holds. It could not fail, and the comment now says why rather than leaving a
  check that reports coverage it does not have.
- QA-627 scrubs NODE_OPTIONS and DOTENV_CONFIG_PATH, which can re-inject HARPER_CLI_*
  from outside the throwaway cwd after the scrub.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZPy6UYZtwxhzLgimHWbGD

* test: make the QA anchors assert what their names claim

- QA-716 Q3 now asserts the long-transaction monitor actually reached the abandoned
  iterator (#1860's release-only branch logs it at warn). Without that the arm was
  blind: dropping the lingering transaction from write supervision, or renaming
  storage.maxTransactionOpenTime, would leave it sleeping 6s and re-reading writes Q1
  had already proved durable. RocksDB only — LMDB never defers a commit and correctly
  never logs the line.
- QA-681 Q3's $share filter now wraps the topic Q3 publishes to. With the filter still
  naming Events/stream, a regression that returned the right refusal code while leaving
  an ordinary subscription installed would never be challenged by a publish.
- QA-627's target= URL is built literally instead of through URL setters, which
  percent-encode userinfo while extractTargetCredentials returns it undecoded; a
  password containing @ would otherwise report a #1873 regression that did not happen.
- All three readiness polls use the existing waitForRouteReady helper, which reports
  last status and error on timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZPy6UYZtwxhzLgimHWbGD

* test: isolate the target= URL arm from the ambient admin password

Building the URL by hand was only a partial fix: the credential still reaches
`new URL(target)` inside bin/cliCredentials.ts, which percent-encodes userinfo that
extractTargetCredentials then returns undecoded, so `:` `;` and `=` in a password
break the arm too — not just `@`, which the guard caught. The arm now provisions its
own super_user with an alphanumeric password, so it pins auth precedence rather than
the shape of whatever HDB_ADMIN_PASSWORD happens to be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZPy6UYZtwxhzLgimHWbGD

* test: close MQTT client leak paths in the QA-681 anchor

A connect() that throws partway through an arm left the clients established before it
open, and Q4 never tore down its deliberately-dropped client at all — so a failure
before the drop left a live MQTT socket holding the runner's event loop, which is how a
failing shard turns into a hanging one. Each arm now establishes its connections inside
the try and endQuiet()s every handle, which already tolerates undefined.

The connect() timer handle is hoisted to a declaration ahead of its handlers. No
temporal-dead-zone error was reachable — the listeners are attached after the timer is
initialized, so neither handler can run before it exists — but declaring it first makes
that local rather than an argument about ordering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZPy6UYZtwxhzLgimHWbGD

* test: drop the mqtt.js internal transport access from the QA-681 anchor

end(true) already closes the socket without sending a DISCONNECT, so the extra
(client as any).stream?.destroy?.() bought nothing and would silently stop working if
mqtt.js renamed the property. Removing it is safe to verify rather than assume: the
boundary assertion added earlier requires that the dropped client received nothing
published after the drop, and it holds across repeated runs with recv-before-drop=15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZPy6UYZtwxhzLgimHWbGD

* test: stop swallowing late MQTT errors, and make QA-716 Q3 self-contained

The post-connect `on('error', () => {})` was mandatory — an unhandled 'error' on an
EventEmitter throws — but discarding it meant a broker crash or socket reset surfaced
only as a downstream waitFor timeout, with the real cause gone. It now logs; Q4
destroys a transport deliberately, so this must not fail the run.

Q3 seeds and fulfills its own bucket. Reading Q1's rows made it fail whenever Q1 failed
and left it unrunnable in isolation, while testing nothing Q1 had not already
established — the same coupling already removed from Q2. No arm in the file shares state
now, so the suite-level bindings are gone with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZPy6UYZtwxhzLgimHWbGD

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants