Skip to content

fix(oauth): stop treating a foreign window.opener as a bind flow - #6425

Merged
Calcium-Ion merged 2 commits into
QuantumNous:mainfrom
neimaravila:fix/oauth-callback-mode
Jul 31, 2026
Merged

fix(oauth): stop treating a foreign window.opener as a bind flow#6425
Calcium-Ion merged 2 commits into
QuantumNous:mainfrom
neimaravila:fix/oauth-callback-mode

Conversation

@neimaravila

@neimaravila neimaravila commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

📝 变更描述 / Description

/oauth/:provider decides between an account bind and a plain login with a single signal:

const mode: 'login' | 'bind' =
  typeof window !== 'undefined' && window.opener ? 'bind' : 'login'

window.opener cannot carry that meaning. Any tab opened from an external link (target="_blank", Slack, Teams, mail clients, another site) has a live opener, and that opener survives the cross-origin round trip to the identity provider.

So an ordinary login callback in such a tab is misread as a bind: it posts the bind handshake to a window that speaks no such protocol, renders the "binding your account" screen, and hangs until the 30s deadline in startOAuthBindResponseDeadline fires with "OAuth binding timed out". The backend is never called at allGET /api/oauth/:provider is never issued.

This is not browser-specific or user-specific: it hits every user who reaches the site through a link instead of typing the URL.

Root cause reproduced against a live Keycloak realm:

step observed
tab opened via window.open (= target="_blank") window.opener truthy
navigate cross-origin to Keycloak, then back to /oauth/oidc?code=… window.opener still truthy, opener.closed === false
resulting mode bind — wrong, this was a login

The change

A bind now needs positive proof instead of an inference. The popup opened for a bind is same-origin (about:blank) before it is sent to the provider, so it is stamped in its own sessionStorage. The stamp survives the provider round trip and is scoped to that popup alone, so a login tab can never carry it.

Resolution rules (resolveOAuthCallbackMode): a bind requires both a live opener and our stamp for that exact provider. Everything else resolves to login, deliberately — a login callback recovers on its own, whereas a wrongly assumed bind can only time out.

The decision is extracted into web/src/features/auth/lib/oauth-callback-mode.ts so it is unit-testable; the storage/opener dependencies are structural, matching the existing style in oauth-bind-window.ts.

Affects every provider sharing this callback: OIDC, GitHub, Discord, LinuxDO and custom providers.

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)

🔗 关联任务 / Related Issue

  • No existing issue found; reproduction steps are in Proof of Work below.

✅ 提交前检查项 / Checklist

  • 人工确认: Description written and verified by me; AI assistance disclosed below.
  • 非重复提交: Searched open PRs/issues; no existing fix for this callback-mode detection.
  • Bug fix 说明: Reproduced deterministically; this is a defect, not a design trade-off.
  • 变更理解: Root cause traced end to end, from opener semantics to the 30s handshake deadline.
  • 范围聚焦: 4 files, no unrelated changes.
  • 本地验证: See Proof of Work.
  • 安全合规: No credentials; the stamp holds only a provider slug.

📸 运行证明 / Proof of Work

Reproduction of the root cause (real Keycloak round trip, browser console):

tab opened via window.open      -> openerTruthy: true
after Keycloak round trip       -> openerTruthy: true, openerClosed: false
mode computed by current code   -> "bind"     (should be "login")

Stamp behaves as required (same browser, same round trip):

marker survived round trip      -> "oidc"
marker in the login tab         -> null        (isolated to the popup)

End-to-end against a build of this branch — the previously hanging scenario (tab with a foreign opener → OIDC callback) now takes the login path immediately:

[GET] /api/oauth/oidc?code=…&state=… => [403]

403 is expected here (synthetic state); the point is the request is issued, where before the fix no request was made and the UI sat on the binding screen for 30s.

Checks

bun test    -> 86 pass, 0 fail (16 files), including 6 new cases
tsgo -b     -> clean

New tests cover: bind popup detected as bind; login in a tab with a foreign opener stays login (the regression); marker for another provider ignored; marker without opener; closed opener; storage unavailable.


🤖 AI assistance disclosure

I am not one of this repository's core contributors. This fix was AI-assisted: the root cause was found by instrumenting the real callback in a browser, and I reviewed the diagnosis, the change and the tests before submitting.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved OAuth callback handling by distinguishing login and account-binding flows using the provider and authorization state.
    • Added safeguards for unavailable, blocked, or unwritable browser session storage.
    • Prevented account-binding popups from proceeding when required state tracking fails.
    • Added explicit handling for Telegram account-binding callbacks.
  • Tests

    • Added coverage for valid, stale, mismatched, missing, and inaccessible OAuth session markers.

The /oauth/:provider callback decided between an account bind and a plain
login with `window.opener ? 'bind' : 'login'`. Any tab opened from an
external link (target="_blank", Slack, mail clients, another site) carries
a live opener, and that opener survives the cross-origin round trip to the
identity provider. Such a login callback was therefore misread as a bind:
it posted a handshake to a window that speaks no such protocol, showed the
"binding your account" screen, and hung until the 30s deadline fired with
"OAuth binding timed out" — while the backend was never called at all.

Reproduced against a real Keycloak round trip: a tab opened via window.open
still reports window.opener !== null on the callback, so mode resolved to
'bind' for an ordinary OIDC login.

A bind now requires positive proof: the popup we open for it is same-origin
(about:blank) before being sent to the provider, so we stamp its own
sessionStorage. The stamp rides through the provider round trip and is
scoped to that popup alone, so a login tab can never carry it. Ambiguity
resolves to 'login', which is the recoverable direction.

Affects every provider sharing this callback (OIDC, GitHub, Discord,
LinuxDO, custom).

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a617cd1-f9f3-4b1b-9022-b83744505125

📥 Commits

Reviewing files that changed from the base of the PR and between 94fd100 and ebcd5a0.

📒 Files selected for processing (4)
  • web/src/features/auth/lib/__tests__/oauth-callback-mode.test.ts
  • web/src/features/auth/lib/oauth-callback-mode.ts
  • web/src/features/profile/components/tabs/account-bindings-tab.tsx
  • web/src/routes/oauth/$provider.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/routes/oauth/$provider.tsx

Walkthrough

OAuth bind popups now record provider-and-state markers in sessionStorage. The callback route uses those markers, opener validity, and callback state to select bind or login mode. Storage failures fall back safely to login, and tests cover matching and invalid callback scenarios.

Changes

OAuth callback mode

Layer / File(s) Summary
Callback mode contract and resolver
web/src/features/auth/lib/oauth-callback-mode.ts, web/src/features/auth/lib/__tests__/oauth-callback-mode.test.ts
The storage contract now supports nullable and failure-safe access. Bind markers include the provider and OAuth state. Callback resolution requires matching markers and a live opener. Tests cover valid, stale, mismatched, unavailable, and unwritable storage.
Binding popup marker wiring
web/src/features/profile/components/tabs/account-bindings-tab.tsx
The binding popup records its provider and state before navigation. Initialization stops and closes the popup when storage marking fails.
OAuth callback route integration
web/src/routes/oauth/$provider.tsx
The route extracts a shared callback state, handles Telegram bind callbacks explicitly, and uses the resolver for other browser callbacks. The effect dependencies use the shared state.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • QuantumNous/new-api#6543: Extends the same OAuth bind-mode detection with opener validation and provider/state markers.

Poem

A rabbit stamps the popup trail,
With provider and state in a session-scale.
Matching markers make binds appear,
Mismatches turn to login clear.
Safe storage keeps the flow in sight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main OAuth callback fix: a foreign window.opener no longer triggers bind-flow detection.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
web/src/features/auth/lib/oauth-callback-mode.test.ts (2)

27-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an explicit return type to fakeStorage.

This new helper annotates its parameter but not its return value. Use OAuthModeStorage & { snapshot: () => Record<string, string> } (with a type-only import) to satisfy the repository’s TypeScript typing rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/auth/lib/oauth-callback-mode.test.ts` around lines 27 - 35,
Update fakeStorage with an explicit return type of OAuthModeStorage & {
snapshot: () => Record<string, string> }, adding a type-only import for
OAuthModeStorage from the existing auth storage module.

Source: Coding guidelines


96-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the storage-exception fallback.

The current test only covers storage: null; add a fake whose getItem throws to verify the resolver returns login when storage access fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/features/auth/lib/oauth-callback-mode.test.ts` around lines 96 - 100,
Extend the `resolveOAuthCallbackMode` test for missing storage with a fake
storage object whose `getItem` method throws, and assert the resolver returns
`login` rather than propagating the exception. Keep the existing `storage: null`
case intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/routes/oauth/`$provider.tsx:
- Around line 72-78: Guard the sessionStorage property reads before invoking the
OAuth helpers: in web/src/routes/oauth/$provider.tsx lines 72-78, update the
resolveOAuthCallbackMode call to safely handle a thrown window.sessionStorage
access and use the intended login-safe fallback; in
web/src/features/profile/components/tabs/account-bindings-tab.tsx lines 166-169,
apply the same protection to the popup.sessionStorage read passed to
markOAuthBindPopup. Ensure blocked storage does not escape from either property
access.

---

Nitpick comments:
In `@web/src/features/auth/lib/oauth-callback-mode.test.ts`:
- Around line 27-35: Update fakeStorage with an explicit return type of
OAuthModeStorage & { snapshot: () => Record<string, string> }, adding a
type-only import for OAuthModeStorage from the existing auth storage module.
- Around line 96-100: Extend the `resolveOAuthCallbackMode` test for missing
storage with a fake storage object whose `getItem` method throws, and assert the
resolver returns `login` rather than propagating the exception. Keep the
existing `storage: null` case intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fb93aa4d-f517-4e19-aa1f-f41bc6fedfdb

📥 Commits

Reviewing files that changed from the base of the PR and between 1721144 and 94fd100.

📒 Files selected for processing (4)
  • web/src/features/auth/lib/oauth-callback-mode.test.ts
  • web/src/features/auth/lib/oauth-callback-mode.ts
  • web/src/features/profile/components/tabs/account-bindings-tab.tsx
  • web/src/routes/oauth/$provider.tsx

Comment thread web/src/routes/oauth/$provider.tsx Outdated
@Calcium-Ion

Copy link
Copy Markdown
Member

We’re about to squash and merge this PR. Which identity would you prefer for the squashed commit: Neimar Avila <neimar.avila@vsgroup.com.br> or your GitHub account, [neimaravila](https://github.com/neimaravila)?

@Calcium-Ion
Calcium-Ion merged commit e78e1db into QuantumNous:main Jul 31, 2026
3 checks passed
@neimaravila
neimaravila deleted the fix/oauth-callback-mode branch July 31, 2026 13:30
kimberxu pushed a commit to kimberxu/new-api that referenced this pull request Aug 1, 2026
Upstream 2026-07-31..2026-08-01: deepseek responses api (QuantumNous#6562),
zstd request decompression (QuantumNous#6545), log stream status (QuantumNous#6558),
tiered retry billing settlement (QuantumNous#6518, QuantumNous#6570), OAuth opener fix
(QuantumNous#6425), multipart image edit fix (QuantumNous#6559), public header nav style
(QuantumNous#6557).

Conflict resolution:
- controller/relay.go: keep per-channel rate limit check (custom) and
  upstream PrepareTieredBillingForSelectedGroup call
- relay/common/relay_info.go: keep RequestDebugSnapshot (custom) and
  upstream TieredBillingSnapshot comment
0401lucky pushed a commit to 0401lucky/new-api that referenced this pull request Aug 2, 2026
…ntumNous#6425)

* fix(oauth): stop treating a foreign window.opener as a bind flow

The /oauth/:provider callback decided between an account bind and a plain
login with `window.opener ? 'bind' : 'login'`. Any tab opened from an
external link (target="_blank", Slack, mail clients, another site) carries
a live opener, and that opener survives the cross-origin round trip to the
identity provider. Such a login callback was therefore misread as a bind:
it posted a handshake to a window that speaks no such protocol, showed the
"binding your account" screen, and hung until the 30s deadline fired with
"OAuth binding timed out" — while the backend was never called at all.

Reproduced against a real Keycloak round trip: a tab opened via window.open
still reports window.opener !== null on the callback, so mode resolved to
'bind' for an ordinary OIDC login.

A bind now requires positive proof: the popup we open for it is same-origin
(about:blank) before being sent to the provider, so we stamp its own
sessionStorage. The stamp rides through the provider round trip and is
scoped to that popup alone, so a login tab can never carry it. Ambiguity
resolves to 'login', which is the recoverable direction.

Affects every provider sharing this callback (OIDC, GitHub, Discord,
LinuxDO, custom).

* fix(oauth): harden bind popup detection
bigfish9 added a commit to lanlingxiawu/new-api-er that referenced this pull request Aug 3, 2026
合并上游 16 个提交,主要是 token Auto 分组 (QuantumNous#6590)、deepseek responses API
(QuantumNous#6562)、Bedrock 客户端断开取消 (QuantumNous#6589)、分层重试计费加固 (QuantumNous#6518/QuantumNous#6570)、
zstd 请求解压 (QuantumNous#6545)、OIDC 自定义显示名 (QuantumNous#6012)、日志暴露 stream_status
(QuantumNous#6558)。

33 处冲突的处理:

- 11 个 legacy channel adaptor:上游删除 panic 之后的死代码以配合新增的
  go vet CI,本仓早已把整段 panic 换成返回 ErrLegacyAdaptorNotImplemented,
  已达成同一目的且不会让中继链路 panic,保留本仓实现。
- relay-aws.go 流式循环:两侧改动正交,合并保留 —— 上游的 ctx.Done() 取消
  分支(客户端断开时不再空转上游),加本仓的 finalizeClaudeOnError(提前
  返回时补发流终止符,否则 Claude 格式调用方会一直挂着)。
- 7 个 locale:两侧各自插入相邻 key,按字母序归并;上游 24 个新 key 与本仓
  1278 个 fork key 全部保留,逐一核对无丢失。
- keys / oauth / profile 前端:上游 Auto 分组为主体,叠加本仓改动;
  account-bindings-tab 与 oauth/$provider 保留本仓实现,理由见下。

测试取舍:

- 不引入 model/token_auto_groups_cache_test.go:它依赖上游的 truncateTables,
  该助手全局清表,与本仓行级清理原则冲突(共享库里有开发数据)。
- 不携带上游 controller/token_test.go:其迁移兼容测试会替换 model.DB 并在
  cleanup 里关闭,后续用 harness 连接的测试会拿到已关闭的句柄。按既有约定,
  token_auto_groups_test.go 需要的四个助手放进 zz_upstream_test_shims_test.go,
  其中 openTokenControllerTestDB 增加了 model.DB/LOG_DB 的成对保存还原。
  本仓原 token_test.go 的 controller 行为测试迁到
  gen_ctrl_token_handlers_test.go,与上游文件名脱钩以免再冲突。
- 三处断言随上游行为更新:stream_status 现对日志所有者可见;deepseek 的
  ConvertOpenAIResponsesRequest 已实现不再返回错误;计费路径判定收紧为
  「标记之外还需带对应 usage payload」,并补了三个反向用例锁住该语义。

遗留:oauth 绑定回调仍用 window.opener 判定 bind/login,上游 QuantumNous#6425 已改为
sessionStorage 标记 + state 比对。该修复要求 popup 先以 about:blank 打开再打
标记,本仓四个内置 provider 走 window.open(url) 直开,直接套用会让绑定永远
判成 login,故本次未采纳,需单独改造。
speedxcc pushed a commit to speedxcc/new-api-speed that referenced this pull request Aug 4, 2026
合并官方上游 Calcium-Ion/new-api main 分支的 10 个新 commit:
- Feat/auto group (QuantumNous#6590)
- fix(aws): cancel Bedrock requests on client disconnect (QuantumNous#6589)
- fix(billing): harden tiered retry group-switch billing (QuantumNous#6570)
- fix(billing): settle tiered retries with final group (QuantumNous#6518)
- feat: deepseek responses api (QuantumNous#6562)
- fix(oauth): stop treating a foreign window.opener as a bind flow (QuantumNous#6425)
- fix(relay): preserve multipart image edits for New API channels (QuantumNous#6559)
- feat(logs): expose stream status to log owners (QuantumNous#6558)
- feat: support zstd request decompression (QuantumNous#6545)
- style: use text-sm for public header nav links (QuantumNous#6557)

冲突解决: model/option.go 的 switch case 区域,双方各自新增了选项注册
(我方 GroupPassThrough + 官方 MaxTokenAutoGroups),取并集保留两者。
其余文件均自动合并成功。

验证: go build + go test 全过(relay/helper、middleware、controller)
neimaravila pushed a commit to neimaravila/new-api that referenced this pull request Aug 6, 2026
Upstream v1.0.0-rc.23.

Conflict resolutions:
- service/group.go: take upstream's IsUserSelectableGroup and its original
  comment; our "User Group" label survives untouched.
- OAuth callback files: our fix landed upstream as QuantumNous#6425 in a stronger form
  (bind mark tied to the OAuth state, storage access that survives blocked
  sessionStorage, Telegram bind callbacks). Take upstream wholesale and drop
  our colocated oauth-callback-mode.test.ts, superseded by lib/__tests__/.

pt-BR follow-up for the new upstream strings: 24 frontend keys (Auto group
ordering, OIDC display name) and 3 backend keys (token.auto_groups_*).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ZsKS6An5YHvZTW3cpVTNX
yuqiyi pushed a commit to yuqiyi/new-api that referenced this pull request Aug 16, 2026
* v1.0.0-rc.24: (117 commits)
  CI: enhance release synchronization workflow with optional file syncing
  fix: 修复兑换码额度精度损失 (QuantumNous#6685)
  feat(rate-limit): add user critical rate limit middleware for access token and aff transfer routes
  fix: test Claude/Gemini endpoints with native request format (QuantumNous#6698)
  feat(channels): refine fetched model categorization (QuantumNous#6632)
  Merge commit from fork
  refactor(relay): move replay metadata onto request bodies
  fix(relay): set Request.GetBody so the HTTP/2 transport can transparently retry after an upstream stream reset (QuantumNous#6249)
  Feat/auto group (QuantumNous#6590)
  fix(aws): cancel Bedrock requests on client disconnect (QuantumNous#6589)
  fix(billing): harden tiered retry group-switch billing (QuantumNous#6570)
  fix(billing): settle tiered retries with final group (QuantumNous#6518)
  feat: deepseek responses api (QuantumNous#6562)
  style: use text-sm for public header nav links to match other nav components (QuantumNous#6557)
  fix(oauth): stop treating a foreign window.opener as a bind flow (QuantumNous#6425)
  fix(relay): preserve multipart image edits for New API channels (QuantumNous#6559)
  feat(logs): expose stream status to log owners (QuantumNous#6558)
  feat: support zstd request decompression (QuantumNous#6545)
  fix: preserve Qwen thinking_budget passthrough (QuantumNous#5836)
  feat(oidc): 支持自定义 OIDC 登录显示名称 (QuantumNous#6012)
  ...

# Conflicts:
#	service/text_quota.go
#	web/src/features/models/components/drawers/model-mutate-drawer.tsx
#	web/src/features/pricing/components/model-details.tsx
#	web/src/features/pricing/lib/price.ts
refeiner pushed a commit to wuqiang44444444/new-api that referenced this pull request Aug 17, 2026
…ntumNous#6425)

* fix(oauth): stop treating a foreign window.opener as a bind flow

The /oauth/:provider callback decided between an account bind and a plain
login with `window.opener ? 'bind' : 'login'`. Any tab opened from an
external link (target="_blank", Slack, mail clients, another site) carries
a live opener, and that opener survives the cross-origin round trip to the
identity provider. Such a login callback was therefore misread as a bind:
it posted a handshake to a window that speaks no such protocol, showed the
"binding your account" screen, and hung until the 30s deadline fired with
"OAuth binding timed out" — while the backend was never called at all.

Reproduced against a real Keycloak round trip: a tab opened via window.open
still reports window.opener !== null on the callback, so mode resolved to
'bind' for an ordinary OIDC login.

A bind now requires positive proof: the popup we open for it is same-origin
(about:blank) before being sent to the provider, so we stamp its own
sessionStorage. The stamp rides through the provider round trip and is
scoped to that popup alone, so a login tab can never carry it. Ambiguity
resolves to 'login', which is the recoverable direction.

Affects every provider sharing this callback (OIDC, GitHub, Discord,
LinuxDO, custom).

* fix(oauth): harden bind popup detection
330079598 pushed a commit to 330079598/new-api that referenced this pull request Aug 19, 2026
…ntumNous#6425)

* fix(oauth): stop treating a foreign window.opener as a bind flow

The /oauth/:provider callback decided between an account bind and a plain
login with `window.opener ? 'bind' : 'login'`. Any tab opened from an
external link (target="_blank", Slack, mail clients, another site) carries
a live opener, and that opener survives the cross-origin round trip to the
identity provider. Such a login callback was therefore misread as a bind:
it posted a handshake to a window that speaks no such protocol, showed the
"binding your account" screen, and hung until the 30s deadline fired with
"OAuth binding timed out" — while the backend was never called at all.

Reproduced against a real Keycloak round trip: a tab opened via window.open
still reports window.opener !== null on the callback, so mode resolved to
'bind' for an ordinary OIDC login.

A bind now requires positive proof: the popup we open for it is same-origin
(about:blank) before being sent to the provider, so we stamp its own
sessionStorage. The stamp rides through the provider round trip and is
scoped to that popup alone, so a login tab can never carry it. Ambiguity
resolves to 'login', which is the recoverable direction.

Affects every provider sharing this callback (OIDC, GitHub, Discord,
LinuxDO, custom).

* fix(oauth): harden bind popup detection
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.

3 participants